Merge "MWExceptionRenderer: Wrap error message in a paragraph"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 15 Nov 2017 10:38:42 +0000 (10:38 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 15 Nov 2017 10:38:43 +0000 (10:38 +0000)
101 files changed:
.mailmap
CREDITS
RELEASE-NOTES-1.30
RELEASE-NOTES-1.31
api.php
autoload.php
composer.json
includes/Feed.php
includes/Html.php
includes/MediaWikiServices.php
includes/Message.php
includes/Preferences.php
includes/ServiceWiring.php
includes/SiteConfiguration.php
includes/api/ApiBase.php
includes/api/ApiFormatBase.php
includes/api/ApiFormatRaw.php
includes/api/ApiHelp.php
includes/api/ApiLogin.php
includes/api/ApiQuery.php
includes/api/ApiStashEdit.php
includes/api/i18n/cs.json
includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
includes/composer/ComposerVendorHtaccessCreator.php [new file with mode: 0644]
includes/exception/MWException.php
includes/exception/MWExceptionRenderer.php
includes/externalstore/ExternalStore.php
includes/externalstore/ExternalStoreFactory.php [new file with mode: 0644]
includes/filerepo/file/File.php
includes/installer/DatabaseUpdater.php
includes/installer/PostgresUpdater.php
includes/libs/http/HttpAcceptNegotiator.php
includes/media/SVG.php
includes/page/Article.php
includes/page/ImagePage.php
includes/skins/SkinFallbackTemplate.php
includes/specials/SpecialEditTags.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialPreferences.php
includes/specials/SpecialRecentchangeslinked.php
includes/specials/SpecialResetTokens.php
includes/specials/SpecialRevisiondelete.php
includes/specials/SpecialSearch.php
includes/specials/forms/PreferencesForm.php
includes/user/BotPassword.php
includes/watcheditem/WatchedItemStore.php
includes/watcheditem/WatchedItemStoreInterface.php [new file with mode: 0644]
languages/LanguageConverter.php
languages/i18n/ais.json
languages/i18n/ar.json
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/ca.json
languages/i18n/de.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/et.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gom-latn.json
languages/i18n/he.json
languages/i18n/hu.json
languages/i18n/it.json
languages/i18n/jv.json
languages/i18n/ko.json
languages/i18n/lad.json
languages/i18n/lv.json
languages/i18n/mr.json
languages/i18n/pt-br.json
languages/i18n/skr-arab.json
languages/i18n/su.json
languages/i18n/tyv.json
languages/i18n/ur.json
languages/i18n/zh-hans.json
maintenance/update.php
resources/Resources.php
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.legacy/oldshared.css
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.skinning/content.externallinks.css
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
tests/common/TestsAutoLoader.php
tests/parser/parserTests.txt
tests/phpunit/includes/PreferencesTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/actions/ActionTest.php
tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php
tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/externalstore/ExternalStoreForTesting.php [new file with mode: 0644]
tests/phpunit/includes/externalstore/ExternalStoreTest.php
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
tests/phpunit/includes/media/SVGTest.php
tests/phpunit/includes/specials/SpecialPageTestBase.php
tests/phpunit/languages/LanguageCodeTest.php
tests/phpunit/languages/LanguageConverterTest.php
tests/selenium/pageobjects/preferences.page.js

index 5a76fb9..c4f8604 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -413,8 +413,9 @@ Sumit Asthana <asthana.sumit23@gmail.com>
 TerraCodes <terracodes@tools.wmflabs.org>
 Thalia Chan <thalia@cantorion.org>
 Thalia Chan <thalia@cantorion.org> <thalia.e.chan@googlemail.com>
-Thiemo Mättig <thiemo.maettig@wikimedia.de>
-Thiemo Mättig <thiemo.maettig@wikimedia.de> <mr.heat@gmx.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de> <thiemo.maettig@wikimedia.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de> <mr.heat@gmx.de>
 This, that and the other <at.light@live.com.au>
 tholam <t.lam@lamsinfosystem.com>
 Thomas Bleher <ThomasBleher@gmx.de> <tbleher@users.mediawiki.org>
diff --git a/CREDITS b/CREDITS
index c38c3fc..6ab4ad3 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -625,7 +625,7 @@ The following list can be found parsed under Special:Version/Credits -->
 * The Discoverer
 * The Evil IP address
 * theopolisme
-* Thiemo Mättig
+* Thiemo Kreuz
 * This, that and the other
 * tholam
 * Thomas Arrow
index d92c38c..1449dab 100644 (file)
@@ -87,6 +87,7 @@ section).
 * Updated OOjs from v2.0.0 to v2.1.0.
 * Updated OOUI from v0.21.1 to v0.23.0.
 * Updated QUnit from v1.23.1 to v2.4.0.
+* Updated phpunit/phpunit from v4.8.35 to v4.8.36.
 
 ==== New external libraries ====
 * The class \TestingAccessWrapper has been moved to the external library
index 9ded68c..18dfc42 100644 (file)
@@ -20,11 +20,14 @@ production.
 === New features in 1.31 ===
 * Wikimedia\Rdbms\IDatabase->select() and similar methods now support
   joins with parentheses for grouping.
+* As a first pass in standardizing dialog boxes across the MediaWiki product,
+Html class now provides helper methods for messageBox, successBox, errorBox and
+warningBox generation.
 
 === External library changes in 1.31 ===
 
 ==== Upgraded external libraries ====
-* Updated dev dependancy phpunit/phpunit from v4.8.35 to v4.8.36.
+* 
 
 ==== New external libraries ====
 * …
diff --git a/api.php b/api.php
index a6ce3b2..d9a69db 100644 (file)
--- a/api.php
+++ b/api.php
@@ -44,6 +44,17 @@ if ( !$wgRequest->checkUrlExtension() ) {
        return;
 }
 
+// Pathinfo can be used for stupid things. We don't support it for api.php at
+// all, so error out if it's present.
+if ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
+       $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValues() );
+       $correctUrl = wfExpandUrl( $correctUrl, PROTO_CANONICAL );
+       header( "Location: $correctUrl", true, 301 );
+       echo 'This endpoint does not support "path info", i.e. extra text between "api.php"'
+               . 'and the "?". Remove any such text and try again.';
+       die( 1 );
+}
+
 // Verify that the API has not been disabled
 if ( !$wgEnableAPI ) {
        header( $_SERVER['SERVER_PROTOCOL'] . ' 500 MediaWiki configuration Error', true, 500 );
index edac2c5..a826f7a 100644 (file)
@@ -284,6 +284,7 @@ $wgAutoloadLocalClasses = [
        'ComposerJson' => __DIR__ . '/includes/libs/composer/ComposerJson.php',
        'ComposerLock' => __DIR__ . '/includes/libs/composer/ComposerLock.php',
        'ComposerPackageModifier' => __DIR__ . '/includes/composer/ComposerPackageModifier.php',
+       'ComposerVendorHtaccessCreator' => __DIR__ . '/includes/composer/ComposerVendorHtaccessCreator.php',
        'ComposerVersionNormalizer' => __DIR__ . '/includes/composer/ComposerVersionNormalizer.php',
        'CompressOld' => __DIR__ . '/maintenance/storage/compressOld.php',
        'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
@@ -454,6 +455,7 @@ $wgAutoloadLocalClasses = [
        'ExtensionRegistry' => __DIR__ . '/includes/registration/ExtensionRegistry.php',
        'ExternalStore' => __DIR__ . '/includes/externalstore/ExternalStore.php',
        'ExternalStoreDB' => __DIR__ . '/includes/externalstore/ExternalStoreDB.php',
+       'ExternalStoreFactory' => __DIR__ . '/includes/externalstore/ExternalStoreFactory.php',
        'ExternalStoreHttp' => __DIR__ . '/includes/externalstore/ExternalStoreHttp.php',
        'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php',
        'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php',
@@ -1595,6 +1597,7 @@ $wgAutoloadLocalClasses = [
        'WatchedItemQueryService' => __DIR__ . '/includes/watcheditem/WatchedItemQueryService.php',
        'WatchedItemQueryServiceExtension' => __DIR__ . '/includes/watcheditem/WatchedItemQueryServiceExtension.php',
        'WatchedItemStore' => __DIR__ . '/includes/watcheditem/WatchedItemStore.php',
+       'WatchedItemStoreInterface' => __DIR__ . '/includes/watcheditem/WatchedItemStoreInterface.php',
        'WatchlistCleanup' => __DIR__ . '/maintenance/cleanupWatchlist.php',
        'WebInstaller' => __DIR__ . '/includes/installer/WebInstaller.php',
        'WebInstallerComplete' => __DIR__ . '/includes/installer/WebInstallerComplete.php',
index 71c9398..a5501d0 100644 (file)
@@ -79,7 +79,8 @@
        },
        "autoload": {
                "psr-0": {
-                       "ComposerHookHandler": "includes/composer"
+                       "ComposerHookHandler": "includes/composer",
+                       "ComposerVendorHtaccessCreator": "includes/composer"
                },
                "files": [
                        "includes/compat/Timestamp.php"
@@ -97,6 +98,8 @@
                "fix": "phpcbf",
                "pre-install-cmd": "ComposerHookHandler::onPreInstall",
                "pre-update-cmd": "ComposerHookHandler::onPreUpdate",
+               "post-install-cmd": "ComposerVendorHtaccessCreator::onEvent",
+               "post-update-cmd": "ComposerVendorHtaccessCreator::onEvent",
                "test": [
                        "composer lint",
                        "composer phpcs"
index bc7747f..fd223e6 100644 (file)
@@ -230,6 +230,12 @@ abstract class ChannelFeed extends FeedItem {
                $wgOut->disable();
                $mimetype = $this->contentType();
                header( "Content-type: $mimetype; charset=UTF-8" );
+
+               // Set a sane filename
+               $exts = MimeMagic::singleton()->getExtensionsForType( $mimetype );
+               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
+               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
+
                if ( $wgVaryOnXFP ) {
                        $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
                }
index 0988b05..524fdcd 100644 (file)
@@ -675,6 +675,52 @@ class Html {
                return self::input( $name, $value, 'checkbox', $attribs );
        }
 
+       /**
+        * Return the HTML for a message box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @param string $className corresponding to box
+        * @param string $heading (optional)
+        * @return string of HTML representing a box.
+        */
+       public static function messageBox( $html, $className, $heading = '' ) {
+               if ( $heading ) {
+                       $html = self::element( 'h2', [], $heading ) . $html;
+               }
+               return self::rawElement( 'div', [ 'class' => $className ], $html );
+       }
+
+       /**
+        * Return a warning box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @return string of HTML representing a warning box.
+        */
+       public static function warningBox( $html ) {
+               return self::messageBox( $html, 'warningbox' );
+       }
+
+       /**
+        * Return an error box.
+        * @since 1.31
+        * @param string $html of contents of error box
+        * @param string $heading (optional)
+        * @return string of HTML representing an error box.
+        */
+       public static function errorBox( $html, $heading = '' ) {
+               return self::messageBox( $html, 'errorbox', $heading );
+       }
+
+       /**
+        * Return a success box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @return string of HTML representing a success box.
+        */
+       public static function successBox( $html ) {
+               return self::messageBox( $html, 'successbox' );
+       }
+
        /**
         * Convenience function to produce a radio button (input element with type=radio)
         *
index 0d010b4..19b71f1 100644 (file)
@@ -31,7 +31,7 @@ use SearchEngineConfig;
 use SearchEngineFactory;
 use SiteLookup;
 use SiteStore;
-use WatchedItemStore;
+use WatchedItemStoreInterface;
 use WatchedItemQueryService;
 use SkinFactory;
 use TitleFormatter;
@@ -513,7 +513,7 @@ class MediaWikiServices extends ServiceContainer {
 
        /**
         * @since 1.28
-        * @return WatchedItemStore
+        * @return WatchedItemStoreInterface
         */
        public function getWatchedItemStore() {
                return $this->getService( 'WatchedItemStore' );
@@ -690,6 +690,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'ShellCommandFactory' );
        }
 
+       /**
+        * @since 1.31
+        * @return \ExternalStoreFactory
+        */
+       public function getExternalStoreFactory() {
+               return $this->getService( 'ExternalStoreFactory' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index 2a55d0e..3b2f3cc 100644 (file)
@@ -1123,11 +1123,29 @@ class Message implements MessageSpecifier, Serializable {
         * @return string
         */
        protected function replaceParameters( $message, $type = 'before', $format ) {
+               // A temporary marker for $1 parameters that is only valid
+               // in non-attribute contexts. However if the entire message is escaped
+               // then we don't want to use it because it will be mangled in all contexts
+               // and its unnessary as ->escaped() messages aren't html.
+               $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
                $replacementKeys = [];
                foreach ( $this->parameters as $n => $param ) {
                        list( $paramType, $value ) = $this->extractParam( $param, $format );
-                       if ( $type === $paramType ) {
-                               $replacementKeys['$' . ( $n + 1 )] = $value;
+                       if ( $type === 'before' ) {
+                               if ( $paramType === 'before' ) {
+                                       $replacementKeys['$' . ( $n + 1 )] = $value;
+                               } else /* $paramType === 'after' */ {
+                                       // To protect against XSS from replacing parameters
+                                       // inside html attributes, we convert $1 to $'"1.
+                                       // In the event that one of the parameters ends up
+                                       // in an attribute, either the ' or the " will be
+                                       // escaped, breaking the replacement and avoiding XSS.
+                                       $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
+                               }
+                       } else {
+                               if ( $paramType === 'after' ) {
+                                       $replacementKeys[$marker . ( $n + 1 )] = $value;
+                               }
                        }
                }
                $message = strtr( $message, $replacementKeys );
index 94854fa..738f8ee 100644 (file)
@@ -82,6 +82,11 @@ class Preferences {
                        return self::$defaultPreferences;
                }
 
+               OutputPage::setupOOUI(
+                       strtolower( $context->getSkin()->getSkinName() ),
+                       $context->getLanguage()->getDir()
+               );
+
                $defaultPreferences = [];
 
                self::profilePreferences( $user, $context, $defaultPreferences );
@@ -320,14 +325,17 @@ class Preferences {
                if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
                        new PasswordAuthenticationRequest(), false )->isGood()
                ) {
-                       $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
-                               $context->msg( 'prefs-resetpass' )->text(), [],
-                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+                       $link = new OOUI\ButtonWidget( [
+                               'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
+                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                               ] ),
+                               'label' => $context->msg( 'prefs-resetpass' )->text(),
+                       ] );
 
                        $defaultPreferences['password'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $link,
+                               'default' => (string)$link,
                                'label-message' => 'yourpassword',
                                'section' => 'personal/info',
                        ];
@@ -471,16 +479,15 @@ class Preferences {
 
                                $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
                                if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
-                                       $link = $linkRenderer->makeLink(
-                                               SpecialPage::getTitleFor( 'ChangeEmail' ),
-                                               $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
-                                               [],
-                                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
-                                       $emailAddress .= $emailAddress == '' ? $link : (
-                                               $context->msg( 'word-separator' )->escaped()
-                                               . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
-                                       );
+                                       $link = new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
+                                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                               ] ),
+                                               'label' =>
+                                                       $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+                                       ] );
+
+                                       $emailAddress .= $emailAddress == '' ? $link : ( '<br />' . $link );
                                }
 
                                $defaultPreferences['emailaddress'] = [
@@ -515,10 +522,10 @@ class Preferences {
                                        } else {
                                                $disableEmailPrefs = true;
                                                $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
-                                                       $linkRenderer->makeKnownLink(
-                                                               SpecialPage::getTitleFor( 'Confirmemail' ),
-                                                               $context->msg( 'emailconfirmlink' )->text()
-                                                       ) . '<br />';
+                                                       new OOUI\ButtonWidget( [
+                                                               'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
+                                                               'label' => $context->msg( 'emailconfirmlink' )->text(),
+                                                       ] );
                                                $emailauthenticationclass = "mw-email-not-authenticated";
                                        }
                                } else {
@@ -755,6 +762,7 @@ class Preferences {
                        'default' => $tzSetting,
                        'size' => 20,
                        'section' => 'rendering/timeoffset',
+                       'id' => 'wpTimeCorrection',
                ];
        }
 
@@ -997,7 +1005,7 @@ class Preferences {
 
                # # Watchlist #####################################
                if ( $user->isAllowed( 'editmywatchlist' ) ) {
-                       $editWatchlistLinks = [];
+                       $editWatchlistLinks = '';
                        $editWatchlistModes = [
                                'edit' => [ 'EditWatchlist', false ],
                                'raw' => [ 'EditWatchlist', 'raw' ],
@@ -1006,16 +1014,19 @@ class Preferences {
                        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                        foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
                                // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
-                               $editWatchlistLinks[] = $linkRenderer->makeKnownLink(
-                                       SpecialPage::getTitleFor( $mode[0], $mode[1] ),
-                                       new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
-                               );
+                               $editWatchlistLinks .=
+                                       new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( $mode[0], $mode[1] )->getLinkURL(),
+                                               'label' => new OOUI\HtmlSnippet(
+                                                       $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
+                                               ),
+                                       ] );
                        }
 
                        $defaultPreferences['editwatchlist'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
+                               'default' => $editWatchlistLinks,
                                'label-message' => 'prefs-editwatchlist-label',
                                'section' => 'watchlist/editwatchlist',
                        ];
@@ -1138,6 +1149,12 @@ class Preferences {
                                'default' => $user->getTokenFromOption( 'watchlisttoken' ),
                                'help-message' => 'prefs-help-watchlist-token2',
                        ];
+                       $defaultPreferences['watchlisttoken-info2'] = [
+                               'type' => 'info',
+                               'section' => 'watchlist/tokenwatchlist',
+                               'raw' => true,
+                               'default' => $context->msg( 'prefs-help-watchlist-token2' )->parse(),
+                       ];
                }
        }
 
@@ -1358,6 +1375,9 @@ class Preferences {
                $formClass = 'PreferencesForm',
                array $remove = []
        ) {
+               // We use ButtonWidgets in some of the getPreferences() functions
+               $context->getOutput()->enableOOUI();
+
                $formDescriptor = self::getPreferences( $user, $context );
                if ( count( $remove ) ) {
                        $removeKeys = array_flip( $remove );
index 0496b67..ae88d37 100644 (file)
@@ -447,6 +447,14 @@ return [
                return $factory;
        },
 
+       'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
+               $config = $services->getMainConfig();
+
+               return new ExternalStoreFactory(
+                       $config->get( 'ExternalStores' )
+               );
+       },
+
        ///////////////////////////////////////////////////////////////////////////
        // 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 7a01a65..2d1d961 100644 (file)
@@ -556,7 +556,7 @@ class SiteConfiguration {
                                ]
                        );
                        // ulimit5.sh breaks this call
-                       $data = trim( wfShellExec( $cmd, $retVal, [], [ 'memory' => 0 ] ) );
+                       $data = trim( wfShellExec( $cmd, $retVal, [], [ 'memory' => 0, 'filesize' => 0 ] ) );
                        if ( $retVal != 0 || !strlen( $data ) ) {
                                throw new MWException( "Failed to run getConfiguration.php." );
                        }
index 80aeff5..bf2b977 100644 (file)
@@ -1069,10 +1069,10 @@ abstract class ApiBase extends ContextSource {
                        } else {
                                $type = 'NULL'; // allow everything
                        }
+               }
 
-                       if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) {
-                               $this->getMain()->markParamsSensitive( $encParamName );
-                       }
+               if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) {
+                       $this->getMain()->markParamsSensitive( $encParamName );
                }
 
                if ( $type == 'boolean' ) {
index 06eaa19..c5f2fcf 100644 (file)
@@ -64,6 +64,26 @@ abstract class ApiFormatBase extends ApiBase {
         */
        abstract public function getMimeType();
 
+       /**
+        * Return a filename for this module's output.
+        * @note If $this->getIsWrappedHtml() || $this->getIsHtml(), you'll very
+        *  likely want to fall back to this class's version.
+        * @since 1.27
+        * @return string Generally this should be "api-result.$ext", and must be
+        *  encoded for inclusion in a Content-Disposition header's filename parameter.
+        */
+       public function getFilename() {
+               if ( $this->getIsWrappedHtml() ) {
+                       return 'api-result-wrapped.json';
+               } elseif ( $this->getIsHtml() ) {
+                       return 'api-result.html';
+               } else {
+                       $exts = MimeMagic::singleton()->getExtensionsForType( $this->getMimeType() );
+                       $ext = $exts ? strtok( $exts, ' ' ) : strtolower( $this->mFormat );
+                       return "api-result.$ext";
+               }
+       }
+
        /**
         * Get the internal format name
         * @return string
@@ -192,6 +212,13 @@ abstract class ApiFormatBase extends ApiBase {
                if ( $apiFrameOptions ) {
                        $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" );
                }
+
+               // Set a Content-Disposition header so something downloading an API
+               // response uses a halfway-sensible filename (T128209).
+               $filename = $this->getFilename();
+               $this->getMain()->getRequest()->response()->header(
+                       "Content-Disposition: inline; filename=\"{$filename}\""
+               );
        }
 
        /**
index 228b47e..ebaeb2c 100644 (file)
@@ -60,6 +60,17 @@ class ApiFormatRaw extends ApiFormatBase {
                return $data['mime'];
        }
 
+       public function getFilename() {
+               $data = $this->getResult()->getResultData();
+               if ( isset( $data['error'] ) ) {
+                       return $this->errorFallback->getFilename();
+               } elseif ( !isset( $data['filename'] ) || $this->getIsWrappedHtml() || $this->getIsHtml() ) {
+                       return parent::getFilename();
+               } else {
+                       return $data['filename'];
+               }
+       }
+
        public function initPrinter( $unused = false ) {
                $data = $this->getResult()->getResultData();
                if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
index 318555a..ea4f724 100644 (file)
@@ -62,6 +62,7 @@ class ApiHelp extends ApiBase {
                if ( $params['wrap'] ) {
                        $data = [
                                'mime' => 'text/html',
+                               'filename' => 'api-help.html',
                                'help' => $html,
                        ];
                        ApiResult::setSubelementsList( $data, 'help' );
@@ -70,6 +71,7 @@ class ApiHelp extends ApiBase {
                        $result->reset();
                        $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'filename', 'api-help.html', ApiResult::NO_SIZE_CHECK );
                }
        }
 
index aa7e25e..9636789 100644 (file)
@@ -134,7 +134,7 @@ class ApiLogin extends ApiBase {
                                $session = $status->getValue();
                                $authRes = 'Success';
                                $loginType = 'BotPassword';
-                       } elseif ( !$botLoginData[2] ) {
+                       } elseif ( !$botLoginData[2] || $status->hasMessage( 'login-throttled' ) ) {
                                $authRes = 'Failed';
                                $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authentication' )->info(
index 44a46b8..31bcc7a 100644 (file)
@@ -459,6 +459,7 @@ class ApiQuery extends ApiBase {
                        // Raw formatter will handle this
                        $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK );
                } else {
                        $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] );
index 8a9de06..4bd6a3f 100644 (file)
@@ -209,8 +209,10 @@ class ApiStashEdit extends ApiBase {
                        Hooks::run( 'ParserOutputStashForEdit',
                                [ $page, $content, $editInfo->output, $summary, $user ] );
 
+                       $titleStr = (string)$title;
                        if ( $alreadyCached ) {
-                               $logger->debug( "Already cached parser output for key '$key' ('$title')." );
+                               $logger->debug( "Already cached parser output for key '{cachekey}' ('{title}').",
+                                       [ 'cachekey' => $key, 'title' => $titleStr ] );
                                return self::ERROR_NONE;
                        }
 
@@ -224,14 +226,17 @@ class ApiStashEdit extends ApiBase {
                        if ( $stashInfo ) {
                                $ok = $cache->set( $key, $stashInfo, $ttl );
                                if ( $ok ) {
-                                       $logger->debug( "Cached parser output for key '$key' ('$title')." );
+                                       $logger->debug( "Cached parser output for key '{cachekey}' ('{title}').",
+                                               [ 'cachekey' => $key, 'title' => $titleStr ] );
                                        return self::ERROR_NONE;
                                } else {
-                                       $logger->error( "Failed to cache parser output for key '$key' ('$title')." );
+                                       $logger->error( "Failed to cache parser output for key '{cachekey}' ('{title}').",
+                                               [ 'cachekey' => $key, 'title' => $titleStr ] );
                                        return self::ERROR_CACHE;
                                }
                        } else {
-                               $logger->info( "Uncacheable parser output for key '$key' ('$title') [$code]." );
+                               $logger->info( "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].",
+                                       [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
                                return self::ERROR_UNCACHEABLE;
                        }
                }
index fcb4af4..ea42e24 100644 (file)
        "api-help-permissions-granted-to": "Uděleno {{PLURAL:$1|skupině|skupinám}}: $2",
        "api-help-right-apihighlimits": "Používání vyšších limitů v API dotazech (pomalé dotazy: $1, rychlé dotazy: $2). Limity pro pomalé dotazy se vztahují i na vícehodnotové parametry.",
        "api-help-open-in-apisandbox": "<small>[otevřít v pískovišti]</small>",
+       "apierror-mustbeloggedin": "Abyste mohli $1, musíte být přihlášeni.",
        "apierror-nosuchsection-what": "$2 neobsahuje sekci $1.",
        "apierror-sectionsnotsupported-what": "$1 nepodporuje sekce.",
        "apierror-timeout": "Server neodpověděl v očekávaném čase.",
index 7f93c12..86a6aae 100644 (file)
@@ -96,7 +96,10 @@ class LocalPasswordPrimaryAuthenticationProvider
                        __METHOD__
                );
                if ( !$row ) {
-                       return AuthenticationResponse::newAbstain();
+                       // Do not reveal whether its bad username or
+                       // bad password to prevent username enumeration
+                       // on private wikis. (T134100)
+                       return $this->failResponse( $req );
                }
 
                $oldRow = clone $row;
diff --git a/includes/composer/ComposerVendorHtaccessCreator.php b/includes/composer/ComposerVendorHtaccessCreator.php
new file mode 100644 (file)
index 0000000..1e5efdf
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.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.
+ *
+ */
+
+/**
+ * Creates a .htaccess in the vendor/ directory
+ * to prevent web access.
+ *
+ * This class runs *outside* of the normal MediaWiki
+ * environment and cannot depend upon any MediaWiki
+ * code.
+ */
+class ComposerVendorHtaccessCreator {
+
+       /**
+        * Handle post-install-cmd and post-update-cmd hooks
+        */
+       public static function onEvent() {
+               $fname = dirname( dirname( __DIR__ ) ) . "/vendor/.htaccess";
+               if ( file_exists( $fname ) ) {
+                       // Already exists
+                       return;
+               }
+
+               file_put_contents( $fname, "Deny from all\n" );
+       }
+}
index 8c1f8dc..6d95919 100644 (file)
@@ -102,15 +102,17 @@ class MWException extends Exception {
                } else {
                        $logId = WebRequest::getRequestId();
                        $type = static::class;
-                       return "<div class=\"errorbox\">" .
-                       '[' . $logId . '] ' .
-                       gmdate( 'Y-m-d H:i:s' ) . ": " .
-                       $this->msg( "internalerror-fatal-exception",
-                               "Fatal exception of type $1",
-                               $type,
-                               $logId,
-                               MWExceptionHandler::getURL( $this )
-                       ) . "</div>\n" .
+                       return Html::errorBox(
+                       htmlspecialchars(
+                               '[' . $logId . '] ' .
+                               gmdate( 'Y-m-d H:i:s' ) . ": " .
+                               $this->msg( "internalerror-fatal-exception",
+                                       "Fatal exception of type $1",
+                                       $type,
+                                       $logId,
+                                       MWExceptionHandler::getURL( $this )
+                               )
+                       ) ) .
                        "<!-- Set \$wgShowExceptionDetails = true; " .
                        "at the bottom of LocalSettings.php to show detailed " .
                        "debugging information. -->";
index 3a18cca..b22e87b 100644 (file)
@@ -169,14 +169,15 @@ class MWExceptionRenderer {
                } else {
                        $logId = WebRequest::getRequestId();
                        $html = "<div class=\"errorbox mw-content-ltr\">" .
-                               '[' . $logId . '] ' .
-                               gmdate( 'Y-m-d H:i:s' ) . ": " .
-                               self::msg( "internalerror-fatal-exception",
-                                       "Fatal exception of type $1",
-                                       get_class( $e ),
-                                       $logId,
-                                       MWExceptionHandler::getURL()
-                               ) . "</div>\n" .
+                               htmlspecialchars(
+                                       '[' . $logId . '] ' .
+                                       gmdate( 'Y-m-d H:i:s' ) . ": " .
+                                       self::msg( "internalerror-fatal-exception",
+                                               "Fatal exception of type $1",
+                                               get_class( $e ),
+                                               $logId,
+                                               MWExceptionHandler::getURL()
+                               ) ) . "</div>\n" .
                                "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
                }
 
index 1563baf..3beab29 100644 (file)
@@ -3,6 +3,8 @@
  * @defgroup ExternalStorage ExternalStorage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Interface for data storage in external repositories.
  *
@@ -52,16 +54,9 @@ class ExternalStore {
         * @return ExternalStoreMedium|bool The store class or false on error
         */
        public static function getStoreObject( $proto, array $params = [] ) {
-               global $wgExternalStores;
-
-               if ( !$wgExternalStores || !in_array( $proto, $wgExternalStores ) ) {
-                       return false; // protocol not enabled
-               }
-
-               $class = 'ExternalStore' . ucfirst( $proto );
-
-               // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
-               return class_exists( $class ) ? new $class( $params ) : false;
+               return MediaWikiServices::getInstance()
+                       ->getExternalStoreFactory()
+                       ->getStoreObject( $proto, $params );
        }
 
        /**
diff --git a/includes/externalstore/ExternalStoreFactory.php b/includes/externalstore/ExternalStoreFactory.php
new file mode 100644 (file)
index 0000000..940fb2e
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+/**
+ * @ingroup ExternalStorage
+ */
+class ExternalStoreFactory {
+
+       /**
+        * @var array
+        */
+       private $externalStores;
+
+       /**
+        * @param array $externalStores See $wgExternalStores
+        */
+       public function __construct( array $externalStores ) {
+               $this->externalStores = array_map( 'strtolower', $externalStores );
+       }
+
+       /**
+        * Get an external store object of the given type, with the given parameters
+        *
+        * @param string $proto Type of external storage, should be a value in $wgExternalStores
+        * @param array $params Associative array of ExternalStoreMedium parameters
+        * @return ExternalStoreMedium|bool The store class or false on error
+        */
+       public function getStoreObject( $proto, array $params = [] ) {
+               if ( !$this->externalStores || !in_array( strtolower( $proto ), $this->externalStores ) ) {
+                       // Protocol not enabled
+                       return false;
+               }
+
+               $class = 'ExternalStore' . ucfirst( $proto );
+
+               // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
+               return class_exists( $class ) ? new $class( $params ) : false;
+       }
+
+}
index 54bd0a5..827f4ca 100644 (file)
@@ -581,6 +581,25 @@ abstract class File implements IDBAccessObject {
                }
        }
 
+       /**
+        * Get the language code from the available languages for this file that matches the language
+        * requested by the user
+        *
+        * @param string $userPreferredLanguage
+        * @return string|null
+        */
+       public function getMatchedLanguage( $userPreferredLanguage ) {
+               $handler = $this->getHandler();
+               if ( $handler && method_exists( $handler, 'getMatchedLanguage' ) ) {
+                       return $handler->getMatchedLanguage(
+                               $userPreferredLanguage,
+                               $handler->getAvailableLanguages( $this )
+                       );
+               } else {
+                       return null;
+               }
+       }
+
        /**
         * In files that support multiple language, what is the default language
         * to use if none specified.
index a317822..54ff712 100644 (file)
@@ -340,12 +340,22 @@ abstract class DatabaseUpdater {
         *
         * @param string $tableName The table name
         * @param string $fieldName The field to be modified
-        * @param string $sqlPath The path to the SQL change path
+        * @param string $sqlPath The path to the SQL patch
         */
        public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
                $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
        }
 
+       /**
+        * @since 1.31
+        *
+        * @param string $tableName The table name
+        * @param string $sqlPath The path to the SQL patch
+        */
+       public function modifyExtensionTable( $tableName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
+       }
+
        /**
         *
         * @since 1.20
index 1f17fec..91f569f 100644 (file)
@@ -481,7 +481,7 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ],
                        [ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
                        [ 'addTable', 'comment', 'patch-comment-table.sql' ],
-                       [ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ],
+                       [ 'addIndex', 'site_stats', 'site_stats_pkey', 'patch-site_stats-pk.sql' ],
                ];
        }
 
index 5f8d9a6..84c1182 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+namespace Wikimedia\Http;
+
 /**
  * Utility for negotiating a value from a set of supported values using a preference list.
  * This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
  *
  * @license GPL-2.0+
  * @author Daniel Kinzler
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
-
-namespace Wikimedia\Http;
-
 class HttpAcceptNegotiator {
 
        /**
index bd78b49..2b13893 100644 (file)
@@ -97,19 +97,50 @@ class SvgHandler extends ImageHandler {
                        if ( isset( $metadata['translations'] ) ) {
                                foreach ( $metadata['translations'] as $lang => $langType ) {
                                        if ( $langType === SVGReader::LANG_FULL_MATCH ) {
-                                               $langList[] = $lang;
+                                               $langList[] = strtolower( $lang );
                                        }
                                }
                        }
                }
-               return $langList;
+               return array_unique( $langList );
        }
 
        /**
-        * What language to render file in if none selected.
+        * SVG's systemLanguage matching rules state:
+        * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
+        * by user preferences exactly equals one of the languages given in the value of this parameter,
+        * or if one of the languages indicated by user preferences exactly equals a prefix of one of
+        * the languages given in the value of this parameter such that the first tag character
+        * following the prefix is "-".'
         *
-        * @param File $file
-        * @return string Language code.
+        * Return the first element of $svgLanguages that matches $userPreferredLanguage
+        *
+        * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
+        * @param string $userPreferredLanguage
+        * @param array $svgLanguages
+        * @return string|null
+        */
+       public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
+               foreach ( $svgLanguages as $svgLang ) {
+                       if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
+                               return $svgLang;
+                       }
+                       $trimmedSvgLang = $svgLang;
+                       while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
+                               $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
+                               if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
+                                       return $svgLang;
+                               }
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * What language to render file in if none selected
+        *
+        * @param File $file Language code
+        * @return string
         */
        public function getDefaultRenderLanguage( File $file ) {
                return 'en';
@@ -479,7 +510,7 @@ class SvgHandler extends ImageHandler {
                        return ( $value > 0 );
                } elseif ( $name == 'lang' ) {
                        // Validate $code
-                       if ( $value === '' || !Language::isValidBuiltInCode( $value ) ) {
+                       if ( $value === '' || !Language::isValidCode( $value ) ) {
                                wfDebug( "Invalid user language code\n" );
 
                                return false;
@@ -499,8 +530,7 @@ class SvgHandler extends ImageHandler {
        public function makeParamString( $params ) {
                $lang = '';
                if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
-                       $params['lang'] = strtolower( $params['lang'] );
-                       $lang = "lang{$params['lang']}-";
+                       $lang = 'lang' . strtolower( $params['lang'] ) . '-';
                }
                if ( !isset( $params['width'] ) ) {
                        return false;
@@ -511,7 +541,7 @@ class SvgHandler extends ImageHandler {
 
        public function parseParamString( $str ) {
                $m = false;
-               if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
+               if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
                        return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
                } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
                        return [ 'width' => $m[1], 'lang' => 'en' ];
index df189af..c9dc273 100644 (file)
@@ -590,7 +590,7 @@ class Article implements Page {
                                                        $outputPage->setRobotPolicy( 'noindex,nofollow' );
 
                                                        $errortext = $error->getWikiText( false, 'view-pool-error' );
-                                                       $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
+                                                       $outputPage->addWikiText( Html::errorBox( $errortext ) );
                                                }
                                                # Connection or timeout error
                                                return;
index 639cbd0..c4baae4 100644 (file)
@@ -285,6 +285,19 @@ class ImagePage extends Article {
                return parent::getContentObject();
        }
 
+       private function getLanguageForRendering( WebRequest $request, File $file ) {
+               $handler = $this->displayImg->getHandler();
+
+               $requestLanguage = $request->getVal( 'lang' );
+               if ( !is_null( $requestLanguage ) ) {
+                       if ( $handler && $handler->validateParam( 'lang', $requestLanguage ) ) {
+                               return $requestLanguage;
+                       }
+               }
+
+               return $handler->getDefaultRenderLanguage( $this->displayImg );
+       }
+
        protected function openShowImage() {
                global $wgEnableUploads, $wgSend404Code, $wgSVGMaxSize;
 
@@ -309,14 +322,9 @@ class ImagePage extends Article {
                                $params = [ 'page' => $page ];
                        }
 
-                       $renderLang = $request->getVal( 'lang' );
+                       $renderLang = $this->getLanguageForRendering( $request, $this->displayImg );
                        if ( !is_null( $renderLang ) ) {
-                               $handler = $this->displayImg->getHandler();
-                               if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) {
-                                       $params['lang'] = $renderLang;
-                               } else {
-                                       $renderLang = null;
-                               }
+                               $params['lang'] = $renderLang;
                        }
 
                        $width_orig = $this->displayImg->getWidth( $page );
@@ -544,12 +552,7 @@ EOT
 
                        $renderLangOptions = $this->displayImg->getAvailableLanguages();
                        if ( count( $renderLangOptions ) >= 1 ) {
-                               $currentLanguage = $renderLang;
-                               $defaultLang = $this->displayImg->getDefaultRenderLanguage();
-                               if ( is_null( $currentLanguage ) ) {
-                                       $currentLanguage = $defaultLang;
-                               }
-                               $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) );
+                               $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) );
                        }
 
                        // Add cannot animate thumbnail warning
@@ -1047,60 +1050,31 @@ EOT
         * Output a drop-down box for language options for the file
         *
         * @param array $langChoices Array of string language codes
-        * @param string $curLang Language code file is being viewed in.
-        * @param string $defaultLang Language code that image is rendered in by default
+        * @param string $renderLang Language code for the language we want the file to rendered in.
         * @return string HTML to insert underneath image.
         */
-       protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) {
+       protected function doRenderLangOpt( array $langChoices, $renderLang ) {
                global $wgScript;
-               sort( $langChoices );
-               $curLang = LanguageCode::bcp47( $curLang );
-               $defaultLang = LanguageCode::bcp47( $defaultLang );
                $opts = '';
-               $haveCurrentLang = false;
-               $haveDefaultLang = false;
-
-               // We make a list of all the language choices in the file.
-               // Additionally if the default language to render this file
-               // is not included as being in this file (for example, in svgs
-               // usually the fallback content is the english content) also
-               // include a choice for that. Last of all, if we're viewing
-               // the file in a language not on the list, add it as a choice.
+
+               $matchedRenderLang = $this->displayImg->getMatchedLanguage( $renderLang );
+
                foreach ( $langChoices as $lang ) {
-                       $code = LanguageCode::bcp47( $lang );
-                       $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
-                       if ( $name !== '' ) {
-                               $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
-                       } else {
-                               $display = $code;
-                       }
-                       $opts .= "\n" . Xml::option( $display, $code, $curLang === $code );
-                       if ( $curLang === $code ) {
-                               $haveCurrentLang = true;
-                       }
-                       if ( $defaultLang === $code ) {
-                               $haveDefaultLang = true;
-                       }
-               }
-               if ( !$haveDefaultLang ) {
-                       // Its hard to know if the content is really in the default language, or
-                       // if its just unmarked content that could be in any language.
-                       $opts = Xml::option(
-                                       $this->getContext()->msg( 'img-lang-default' )->text(),
-                               $defaultLang,
-                               $defaultLang === $curLang
-                       ) . $opts;
-               }
-               if ( !$haveCurrentLang && $defaultLang !== $curLang ) {
-                       $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() );
-                       if ( $name !== '' ) {
-                               $display = $this->getContext()->msg( 'img-lang-opt', $curLang, $name )->text();
-                       } else {
-                               $display = $curLang;
-                       }
-                       $opts = Xml::option( $display, $curLang, true ) . $opts;
+                       $opts .= $this->createXmlOptionStringForLanguage(
+                               $lang,
+                               $matchedRenderLang === $lang
+                       );
                }
 
+               // Allow for the default case in an svg <switch> that is displayed if no
+               // systemLanguage attribute matches
+               $opts .= "\n" .
+                       Xml::option(
+                               $this->getContext()->msg( 'img-lang-default' )->text(),
+                               'und',
+                               is_null( $matchedRenderLang )
+                       );
+
                $select = Html::rawElement(
                        'select',
                        [ 'id' => 'mw-imglangselector', 'name' => 'lang' ],
@@ -1119,6 +1093,27 @@ EOT
                return $langSelectLine;
        }
 
+       /**
+        * @param $lang string
+        * @param $selected bool
+        * @return string
+        */
+       private function createXmlOptionStringForLanguage( $lang, $selected ) {
+               $code = LanguageCode::bcp47( $lang );
+               $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
+               if ( $name !== '' ) {
+                       $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
+               } else {
+                       $display = $code;
+               }
+               return "\n" .
+                       Xml::option(
+                               $display,
+                               $lang,
+                               $selected
+                       );
+       }
+
        /**
         * Get the width and height to display image at.
         *
index ee8d841..1ad1ab0 100644 (file)
@@ -96,12 +96,9 @@ class SkinFallbackTemplate extends BaseTemplate {
         * warning message and page content.
         */
        public function execute() {
-               $this->html( 'headelement' ) ?>
-
-               <div class="warningbox">
-                       <?php echo $this->buildHelpfulInformationMessage() ?>
-               </div>
-
+               $this->html( 'headelement' );
+               echo Html::warningBox( $this->buildHelpfulInformationMessage() );
+       ?>
                <form action="<?php $this->text( 'wgScript' ) ?>">
                        <input type="hidden" name="title" value="<?php $this->text( 'searchtitle' ) ?>" />
                        <h3><label for="searchInput"><?php $this->msg( 'search' ) ?></label></h3>
index 476c452..eb0f0aa 100644 (file)
@@ -451,9 +451,8 @@ class SpecialEditTags extends UnlistedSpecialPage {
         */
        protected function failure( $status ) {
                $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
-               $this->getOutput()->addWikiText( '<div class="errorbox">' .
-                       $status->getWikiText( 'tags-edit-failure' ) .
-                       '</div>'
+               $this->getOutput()->addWikiText(
+                       Html::errorBox( $status->getWikiText( 'tags-edit-failure' ) )
                );
                $this->showForm();
        }
index 46d7cf7..02d6d00 100644 (file)
@@ -235,18 +235,18 @@ class MovePageForm extends UnlistedSpecialPage {
                }
 
                if ( count( $err ) ) {
-                       $out->addHTML( "<div class='errorbox'>\n" );
                        $action_desc = $this->msg( 'action-move' )->plain();
-                       $out->addWikiMsg( 'permissionserrorstext-withaction', count( $err ), $action_desc );
+                       $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
+                               count( $err ), $action_desc )->parseAsBlock();
 
                        if ( count( $err ) == 1 ) {
                                $errMsg = $err[0];
                                $errMsgName = array_shift( $errMsg );
 
                                if ( $errMsgName == 'hookaborted' ) {
-                                       $out->addHTML( "<p>{$errMsg[0]}</p>\n" );
+                                       $errMsgHtml .= "<p>{$errMsg[0]}</p>\n";
                                } else {
-                                       $out->addWikiMsgArray( $errMsgName, $errMsg );
+                                       $errMsgHtml .= $this->msg( $errMsgName, $errMsg )->parseAsBlock();
                                }
                        } else {
                                $errStr = [];
@@ -260,9 +260,9 @@ class MovePageForm extends UnlistedSpecialPage {
                                        }
                                }
 
-                               $out->addHTML( '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n" );
+                               $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
                        }
-                       $out->addHTML( "</div>\n" );
+                       $out->addHTML( Html::errorBox( $errMsgHtml ) );
                }
 
                if ( $this->oldTitle->isProtected( 'move' ) ) {
index 8ad1630..7fa74af 100644 (file)
@@ -50,8 +50,8 @@ class SpecialPreferences extends SpecialPage {
                        return;
                }
 
-               $out->addModules( 'mediawiki.special.preferences' );
-               $out->addModuleStyles( 'mediawiki.special.preferences.styles' );
+               $out->addModules( 'mediawiki.special.preferences.ooui' );
+               $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' );
 
                $session = $this->getRequest()->getSession();
                if ( $session->get( 'specialPreferencesSaveSuccess' ) ) {
@@ -83,37 +83,19 @@ class SpecialPreferences extends SpecialPage {
 
                $htmlForm = $this->getFormObject( $user, $this->getContext() );
                $htmlForm->setSubmitCallback( [ 'Preferences', 'tryUISubmit' ] );
-               $sectionTitles = $htmlForm->getPreferenceSections();
-
-               $prefTabs = '';
-               foreach ( $sectionTitles as $key ) {
-                       $prefTabs .= Html::rawElement( 'li',
-                               [
-                                       'role' => 'presentation',
-                                       'class' => ( $key === 'personal' ) ? 'selected' : null
-                               ],
-                               Html::rawElement( 'a',
-                                       [
-                                               'id' => 'preftab-' . $key,
-                                               'role' => 'tab',
-                                               'href' => '#mw-prefsection-' . $key,
-                                               'aria-controls' => 'mw-prefsection-' . $key,
-                                               'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false',
-                                               'tabIndex' => ( $key === 'personal' ) ? 0 : -1,
-                                       ],
-                                       $htmlForm->getLegend( $key )
-                               )
-                       );
+
+               $prefTabs = [];
+               foreach ( $htmlForm->getPreferenceSections() as $key ) {
+                       $prefTabs[] = [
+                               'name' => $key,
+                               'label' => $htmlForm->getLegend( $key ),
+                       ];
                }
+               $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs );
+
+               // TODO: Render fake tabs here to avoid FOUC.
+               // $out->addHTML( $fakeTabs );
 
-               $out->addHTML(
-                       Html::rawElement( 'ul',
-                               [
-                                       'id' => 'preftoc',
-                                       'role' => 'tablist'
-                               ],
-                               $prefTabs )
-               );
                $htmlForm->show();
        }
 
@@ -136,7 +118,7 @@ class SpecialPreferences extends SpecialPage {
 
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage
-               $htmlForm = new HTMLForm( [], $context, 'prefs-restore' );
+               $htmlForm = HTMLForm::factory( 'ooui', [], $context, 'prefs-restore' );
 
                $htmlForm->setSubmitTextMsg( 'restoreprefs' );
                $htmlForm->setSubmitDestructive();
index 99880de..358a309 100644 (file)
@@ -62,8 +62,9 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
                $outputPage = $this->getOutput();
                $title = Title::newFromText( $target );
                if ( !$title || $title->isExternal() ) {
-                       $outputPage->addHTML( '<div class="errorbox">' . $this->msg( 'allpagesbadtitle' )
-                                       ->parse() . '</div>' );
+                       $outputPage->addHTML(
+                               Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse() )
+                       );
 
                        return false;
                }
index 3e89686..964a261 100644 (file)
@@ -74,7 +74,7 @@ class SpecialResetTokens extends FormSpecialPage {
 
        public function onSuccess() {
                $this->getOutput()->wrapWikiMsg(
-                       "<div class='successbox'>\n$1\n</div>",
+                       Html::successBox( '$1' ),
                        'resettokens-done'
                );
        }
index e1d4dd1..8edebf2 100644 (file)
@@ -636,9 +636,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
        protected function failure( $status ) {
                // Messages: revdelete-failure, logdelete-failure
                $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
-               $this->getOutput()->addWikiText( '<div class="errorbox">' .
-                       $status->getWikiText( $this->typeLabels['failure'] ) .
-                       '</div>'
+               $this->getOutput()->addWikiText(
+                       Html::errorBox(
+                               $status->getWikiText( $this->typeLabels['failure'] )
+                       )
                );
                $this->showForm();
        }
index 09210e4..b3a58cb 100644 (file)
@@ -365,16 +365,12 @@ class SpecialSearch extends SpecialPage {
                if ( $hasErrors ) {
                        list( $error, $warning ) = $textStatus->splitByErrorType();
                        if ( $error->getErrors() ) {
-                               $out->addHTML( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'errorbox' ],
+                               $out->addHTML( Html::errorBox(
                                        $error->getHTML( 'search-error' )
                                ) );
                        }
                        if ( $warning->getErrors() ) {
-                               $out->addHTML( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'warningbox' ],
+                               $out->addHTML( Html::warningBox(
                                        $warning->getHTML( 'search-warning' )
                                ) );
                        }
index d4e5ef4..28cfb8b 100644 (file)
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Form to edit user preferences.
  */
-class PreferencesForm extends HTMLForm {
+class PreferencesForm extends OOUIHTMLForm {
        // Override default value from HTMLForm
        protected $mSubSectionBeforeFields = false;
 
@@ -71,8 +69,6 @@ class PreferencesForm extends HTMLForm {
         * @return string
         */
        function getButtons() {
-               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
-
                if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
                        return '';
                }
@@ -82,9 +78,14 @@ class PreferencesForm extends HTMLForm {
                if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
                        $t = $this->getTitle()->getSubpage( 'reset' );
 
-                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-                       $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(),
-                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
+                       $html .= new OOUI\ButtonWidget( [
+                               'infusable' => true,
+                               'id' => 'mw-prefs-restoreprefs',
+                               'label' => $this->msg( 'restoreprefs' )->text(),
+                               'href' => $t->getLinkURL(),
+                               'flags' => [ 'destructive' ],
+                               'framed' => false,
+                       ] );
 
                        $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
                }
index 25625e7..b898d8a 100644 (file)
@@ -437,7 +437,7 @@ class BotPassword implements IDBAccessObject {
         * @return Status On success, the good status's value is the new Session object
         */
        public static function login( $username, $password, WebRequest $request ) {
-               global $wgEnableBotPasswords;
+               global $wgEnableBotPasswords, $wgPasswordAttemptThrottle;
 
                if ( !$wgEnableBotPasswords ) {
                        return Status::newFatal( 'botpasswords-disabled' );
@@ -462,6 +462,20 @@ class BotPassword implements IDBAccessObject {
                        return Status::newFatal( 'nosuchuser', $name );
                }
 
+               // Throttle
+               $throttle = null;
+               if ( !empty( $wgPasswordAttemptThrottle ) ) {
+                       $throttle = new MediaWiki\Auth\Throttler( $wgPasswordAttemptThrottle, [
+                               'type' => 'botpassword',
+                               'cache' => ObjectCache::getLocalClusterInstance(),
+                       ] );
+                       $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
+                       if ( $result ) {
+                               $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
+                               return Status::newFatal( $msg );
+                       }
+               }
+
                // Get the bot password
                $bp = self::newFromUser( $user, $appId );
                if ( !$bp ) {
@@ -480,6 +494,9 @@ class BotPassword implements IDBAccessObject {
                }
 
                // Ok! Create the session.
+               if ( $throttle ) {
+                       $throttle->clear( $user->getName(), $request->getIP() );
+               }
                return Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) );
        }
 }
index 60d8b76..094297c 100644 (file)
@@ -7,11 +7,11 @@ use MediaWiki\MediaWikiServices;
 use Wikimedia\Assert\Assert;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\DBUnexpectedError;
 
 /**
  * Storage layer class for WatchedItems.
- * Database interaction.
+ * Database interaction & caching
+ * TODO caching should be factored out into a CachingWatchedItemStore class
  *
  * Uses database because this uses User::isAnon
  *
@@ -20,10 +20,7 @@ use Wikimedia\Rdbms\DBUnexpectedError;
  * @author Addshore
  * @since 1.27
  */
-class WatchedItemStore implements StatsdAwareInterface {
-
-       const SORT_DESC = 'DESC';
-       const SORT_ASC = 'ASC';
+class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
 
        /**
         * @var LoadBalancer
@@ -216,12 +213,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Count the number of individual items that are watched by the user.
-        * If a subject and corresponding talk page are watched this will return 2.
-        *
-        * @param User $user
-        *
-        * @return int
+        * @since 1.31
         */
        public function countWatchedItems( User $user ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
@@ -238,9 +230,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param LinkTarget $target
-        *
-        * @return int
+        * @since 1.27
         */
        public function countWatchers( LinkTarget $target ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
@@ -258,14 +248,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Number of page watchers who also visited a "recent" edit
-        *
-        * @param LinkTarget $target
-        * @param mixed $threshold timestamp accepted by wfTimestamp
-        *
-        * @return int
-        * @throws DBUnexpectedError
-        * @throws MWException
+        * @since 1.27
         */
        public function countVisitingWatchers( LinkTarget $target, $threshold ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
@@ -286,13 +269,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param LinkTarget[] $targets
-        * @param array $options Allowed keys:
-        *        'minimumWatchers' => int
-        *
-        * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
-        *         All targets will be present in the result. 0 either means no watchers or the number
-        *         of watchers was below the minimumWatchers option if passed.
+        * @since 1.27
         */
        public function countWatchersMultiple( array $targets, array $options = [] ) {
                $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
@@ -325,19 +302,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Number of watchers of each page who have visited recent edits to that page
-        *
-        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed $threshold),
-        *        $threshold is:
-        *        - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
-        *        - null if $target doesn't exist
-        * @param int|null $minimumWatchers
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
-        *         where $watchers is an int:
-        *         - if the page exists, number of users watching who have visited the page recently
-        *         - if the page doesn't exist, number of users that have the page on their watchlist
-        *         - 0 means there are no visiting watchers or their number is below the minimumWatchers
-        *         option (if passed).
+        * @since 1.27
         */
        public function countVisitingWatchersMultiple(
                array $targetsWithVisitThresholds,
@@ -417,12 +382,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Get an item (may be cached)
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
+        * @since 1.27
         */
        public function getWatchedItem( User $user, LinkTarget $target ) {
                if ( $user->isAnon() ) {
@@ -439,12 +399,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Loads an item from the db
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
+        * @since 1.27
         */
        public function loadWatchedItem( User $user, LinkTarget $target ) {
                // Only loggedin user can have a watchlist
@@ -475,13 +430,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $user
-        * @param array $options Allowed keys:
-        *        'forWrite' => bool defaults to false
-        *        'sort' => string optional sorting by namespace ID and title
-        *                     one of the self::SORT_* constants
-        *
-        * @return WatchedItem[]
+        * @since 1.27
         */
        public function getWatchedItemsForUser( User $user, array $options = [] ) {
                $options += [ 'forWrite' => false ];
@@ -522,25 +471,14 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return bool
+        * @since 1.27
         */
        public function isWatched( User $user, LinkTarget $target ) {
                return (bool)$this->getWatchedItem( $user, $target );
        }
 
        /**
-        * @param User $user
-        * @param LinkTarget[] $targets
-        *
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
-        *         where $timestamp is:
-        *         - string|null value of wl_notificationtimestamp,
-        *         - false if $target is not watched by $user.
+        * @since 1.27
         */
        public function getNotificationTimestampsBatch( User $user, array $targets ) {
                $timestamps = [];
@@ -589,20 +527,14 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
+        * @since 1.27
         */
        public function addWatch( User $user, LinkTarget $target ) {
                $this->addWatchBatchForUser( $user, [ $target ] );
        }
 
        /**
-        * @param User $user
-        * @param LinkTarget[] $targets
-        *
-        * @return bool success
+        * @since 1.27
         */
        public function addWatchBatchForUser( User $user, array $targets ) {
                if ( $this->readOnlyMode->isReadOnly() ) {
@@ -651,15 +583,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Removes the an entry for the User watching the LinkTarget
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return bool success
-        * @throws DBUnexpectedError
-        * @throws MWException
+        * @since 1.27
         */
        public function removeWatch( User $user, LinkTarget $target ) {
                // Only logged in user can have a watchlist
@@ -683,11 +607,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $user The user to set the timestamp for
-        * @param string|null $timestamp Set the update timestamp to this value
-        * @param LinkTarget[] $targets List of targets to update. Default to all targets
-        *
-        * @return bool success
+        * @since 1.27
         */
        public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
                // Only loggedin user can have a watchlist
@@ -720,12 +640,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $editor The editor that triggered the update. Their notification
-        *  timestamp will not be updated(they have already seen it)
-        * @param LinkTarget $target The target to update timestamps for
-        * @param string $timestamp Set the update timestamp to this value
-        *
-        * @return int[] Array of user IDs the timestamp has been updated for
+        * @since 1.27
         */
        public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
                $dbw = $this->getConnectionRef( DB_MASTER );
@@ -781,16 +696,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Reset the notification timestamp of this entry
-        *
-        * @param User $user
-        * @param Title $title
-        * @param string $force Whether to force the write query to be executed even if the
-        *    page is not watched or the notification timestamp is already NULL.
-        *    'force' in order to force
-        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
-        *
-        * @return bool success
+        * @since 1.27
         */
        public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
                // Only loggedin user can have a watchlist
@@ -879,11 +785,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $user
-        * @param int $unreadLimit
-        *
-        * @return int|bool The number of unread notifications
-        *                  true if greater than or equal to $unreadLimit
+        * @since 1.27
         */
        public function countUnreadNotifications( User $user, $unreadLimit = null ) {
                $queryOptions = [];
@@ -916,13 +818,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        *
-        * @param LinkTarget $oldTarget
-        * @param LinkTarget $newTarget
+        * @since 1.27
         */
        public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
                $oldTarget = Title::newFromLinkTarget( $oldTarget );
@@ -933,14 +829,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        * This must be called separately for Subject and Talk pages
-        *
-        * @param LinkTarget $oldTarget
-        * @param LinkTarget $newTarget
+        * @since 1.27
         */
        public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
                $dbw = $this->getConnectionRef( DB_MASTER );
diff --git a/includes/watcheditem/WatchedItemStoreInterface.php b/includes/watcheditem/WatchedItemStoreInterface.php
new file mode 100644 (file)
index 0000000..d5a3d7c
--- /dev/null
@@ -0,0 +1,291 @@
+<?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 Watchlist
+ */
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * @author Addshore
+ * @since 1.31 interface created. WatchedItemStore implementation available since 1.27
+ */
+interface WatchedItemStoreInterface {
+
+       /**
+        * @since 1.31
+        */
+       const SORT_ASC = 'ASC';
+
+       /**
+        * @since 1.31
+        */
+       const SORT_DESC = 'DESC';
+
+       /**
+        * Count the number of individual items that are watched by the user.
+        * If a subject and corresponding talk page are watched this will return 2.
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        *
+        * @return int
+        */
+       public function countWatchedItems( User $user );
+
+       /**
+        * @since 1.31
+        *
+        * @param LinkTarget $target
+        *
+        * @return int
+        */
+       public function countWatchers( LinkTarget $target );
+
+       /**
+        * Number of page watchers who also visited a "recent" edit
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $target
+        * @param mixed $threshold timestamp accepted by wfTimestamp
+        *
+        * @return int
+        * @throws DBUnexpectedError
+        * @throws MWException
+        */
+       public function countVisitingWatchers( LinkTarget $target, $threshold );
+
+       /**
+        * @since 1.31
+        *
+        * @param LinkTarget[] $targets
+        * @param array $options Allowed keys:
+        *        'minimumWatchers' => int
+        *
+        * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
+        *         All targets will be present in the result. 0 either means no watchers or the number
+        *         of watchers was below the minimumWatchers option if passed.
+        */
+       public function countWatchersMultiple( array $targets, array $options = [] );
+
+       /**
+        * Number of watchers of each page who have visited recent edits to that page
+        *
+        * @since 1.31
+        *
+        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed
+        *     $threshold),
+        *        $threshold is:
+        *        - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
+        *        - null if $target doesn't exist
+        * @param int|null $minimumWatchers
+        *
+        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
+        *         where $watchers is an int:
+        *         - if the page exists, number of users watching who have visited the page recently
+        *         - if the page doesn't exist, number of users that have the page on their watchlist
+        *         - 0 means there are no visiting watchers or their number is below the
+        *     minimumWatchers
+        *         option (if passed).
+        */
+       public function countVisitingWatchersMultiple(
+               array $targetsWithVisitThresholds,
+               $minimumWatchers = null
+       );
+
+       /**
+        * Get an item (may be cached)
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return WatchedItem|false
+        */
+       public function getWatchedItem( User $user, LinkTarget $target );
+
+       /**
+        * Loads an item from the db
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return WatchedItem|false
+        */
+       public function loadWatchedItem( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'forWrite' => bool defaults to false
+        *        'sort' => string optional sorting by namespace ID and title
+        *                     one of the self::SORT_* constants
+        *
+        * @return WatchedItem[]
+        */
+       public function getWatchedItemsForUser( User $user, array $options = [] );
+
+       /**
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return bool
+        */
+       public function isWatched( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget[] $targets
+        *
+        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
+        *         where $timestamp is:
+        *         - string|null value of wl_notificationtimestamp,
+        *         - false if $target is not watched by $user.
+        */
+       public function getNotificationTimestampsBatch( User $user, array $targets );
+
+       /**
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        */
+       public function addWatch( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget[] $targets
+        *
+        * @return bool success
+        */
+       public function addWatchBatchForUser( User $user, array $targets );
+
+       /**
+        * Removes the an entry for the User watching the LinkTarget
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return bool success
+        * @throws DBUnexpectedError
+        * @throws MWException
+        */
+       public function removeWatch( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user The user to set the timestamp for
+        * @param string|null $timestamp Set the update timestamp to this value
+        * @param LinkTarget[] $targets List of targets to update. Default to all targets
+        *
+        * @return bool success
+        */
+       public function setNotificationTimestampsForUser(
+               User $user,
+               $timestamp,
+               array $targets = []
+       );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $editor The editor that triggered the update. Their notification
+        *  timestamp will not be updated(they have already seen it)
+        * @param LinkTarget $target The target to update timestamps for
+        * @param string $timestamp Set the update timestamp to this value
+        *
+        * @return int[] Array of user IDs the timestamp has been updated for
+        */
+       public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp );
+
+       /**
+        * Reset the notification timestamp of this entry
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param Title $title
+        * @param string $force Whether to force the write query to be executed even if the
+        *    page is not watched or the notification timestamp is already NULL.
+        *    'force' in order to force
+        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is
+        *     assumed.
+        *
+        * @return bool success
+        */
+       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param int $unreadLimit
+        *
+        * @return int|bool The number of unread notifications
+        *                  true if greater than or equal to $unreadLimit
+        */
+       public function countUnreadNotifications( User $user, $unreadLimit = null );
+
+       /**
+        * Check if the given title already is watched by the user, and if so
+        * add a watch for the new title.
+        *
+        * To be used for page renames and such.
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $oldTarget
+        * @param LinkTarget $newTarget
+        */
+       public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+       /**
+        * Check if the given title already is watched by the user, and if so
+        * add a watch for the new title.
+        *
+        * To be used for page renames and such.
+        * This must be called separately for Subject and Talk pages
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $oldTarget
+        * @param LinkTarget $newTarget
+        */
+       public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+}
index 67c0ca7..1f720af 100644 (file)
@@ -20,6 +20,8 @@
  */
 use MediaWiki\MediaWikiServices;
 
+use MediaWiki\Logger\LoggerFactory;
+
 /**
  * Base class for language conversion.
  * @ingroup Language
@@ -351,26 +353,34 @@ class LanguageConverter {
                if ( $this->guessVariant( $text, $toVariant ) ) {
                        return $text;
                }
-
                /* we convert everything except:
-                * 1. HTML markups (anything between < and >)
-                * 2. HTML entities
-                * 3. placeholders created by the parser
-                */
-               $marker = '|' . Parser::MARKER_PREFIX . '[\-a-zA-Z0-9]+';
+                  1. HTML markups (anything between < and >)
+                  2. HTML entities
+                  3. placeholders created by the parser
+                  IMPORTANT: Beware of failure from pcre.backtrack_limit (T124404).
+                  Minimize use of backtracking where possible.
+               */
+               $marker = '|' . Parser::MARKER_PREFIX . '[^\x7f]++\x7f';
 
                // this one is needed when the text is inside an HTML markup
-               $htmlfix = '|<[^>]+$|^[^<>]*>';
+               $htmlfix = '|<[^>\004]++(?=\004$)|^[^<>]*+>';
+
+               // Optimize for the common case where these tags have
+               // few or no children. Thus try and possesively get as much as
+               // possible, and only engage in backtracking when we hit a '<'.
 
                // disable convert to variants between <code> tags
-               $codefix = '<code>.+?<\/code>|';
+               $codefix = '<code>[^<]*+(?:(?:(?!<\/code>).)[^<]*+)*+<\/code>|';
                // disable conversion of <script> tags
-               $scriptfix = '<script.*?>.*?<\/script>|';
+               $scriptfix = '<script[^>]*+>[^<]*+(?:(?:(?!<\/script>).)[^<]*+)*+<\/script>|';
                // disable conversion of <pre> tags
-               $prefix = '<pre.*?>.*?<\/pre>|';
+               $prefix = '<pre[^>]*+>[^<]*+(?:(?:(?!<\/pre>).)[^<]*+)*+<\/pre>|';
+               // The "|.*+)" at the end, is in case we missed some part of html syntax,
+               // we will fail securely (hopefully) by matching the rest of the string.
+               $htmlFullTag = '<(?:[^>=]*+(?>[^>=]*+=\s*+(?:"[^"]*"|\'[^\']*\'|[^\'">\s]*+))*+[^>=]*+>|.*+)|';
 
-               $reg = '/' . $codefix . $scriptfix . $prefix .
-                       '<[^>]+>|&[a-zA-Z#][a-z0-9]+;' . $marker . $htmlfix . '/s';
+               $reg = '/' . $codefix . $scriptfix . $prefix . $htmlFullTag .
+                       '&[a-zA-Z#][a-z0-9]++;' . $marker . $htmlfix . '|\004$/s';
                $startPos = 0;
                $sourceBlob = '';
                $literalBlob = '';
@@ -378,18 +388,45 @@ class LanguageConverter {
                // Guard against delimiter nulls in the input
                // (should never happen: see T159174)
                $text = str_replace( "\000", '', $text );
+               $text = str_replace( "\004", '', $text );
 
                $markupMatches = null;
                $elementMatches = null;
+
+               // We add a marker (\004) at the end of text, to ensure we always match the
+               // entire text (Otherwise, pcre.backtrack_limit might cause silent failure)
                while ( $startPos < strlen( $text ) ) {
-                       if ( preg_match( $reg, $text, $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) {
+                       if ( preg_match( $reg, $text . "\004", $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) {
                                $elementPos = $markupMatches[0][1];
                                $element = $markupMatches[0][0];
+                               if ( $element === "\004" ) {
+                                       // We hit the end.
+                                       $elementPos = strlen( $text );
+                                       $element = '';
+                               } elseif ( substr( $element, -1 ) === "\004" ) {
+                                       // This can sometimes happen if we have
+                                       // unclosed html tags (For example
+                                       // when converting a title attribute
+                                       // during a recursive call that contains
+                                       // a &lt; e.g. <div title="&lt;">.
+                                       $element = substr( $element, 0, -1 );
+                               }
                        } else {
-                               $elementPos = strlen( $text );
-                               $element = '';
+                               // If we hit here, then Language Converter could be tricked
+                               // into doing an XSS, so we refuse to translate.
+                               // If non-crazy input manages to reach this code path,
+                               // we should consider it a bug.
+                               $log = LoggerFactory::getInstance( 'languageconverter' );
+                               $log->error( "Hit pcre.backtrack_limit in " . __METHOD__
+                                       . ". Disabling language conversion for this page.",
+                                       [
+                                               "method" => __METHOD__,
+                                               "variant" => $toVariant,
+                                               "startOfText" => substr( $text, 0, 500 )
+                                       ]
+                               );
+                               return $text;
                        }
-
                        // Queue the part before the markup for translation in a batch
                        $sourceBlob .= substr( $text, $startPos, $elementPos - $startPos ) . "\000";
 
@@ -398,9 +435,16 @@ class LanguageConverter {
 
                        // Translate any alt or title attributes inside the matched element
                        if ( $element !== ''
-                               && preg_match( '/^(<[^>\s]*)\s([^>]*)(.*)$/', $element, $elementMatches )
+                               && preg_match( '/^(<[^>\s]*+)\s([^>]*+)(.*+)$/', $element, $elementMatches )
                        ) {
+                               // FIXME, this decodes entities, so if you have something
+                               // like <div title="foo&lt;bar"> the bar won't get
+                               // translated since after entity decoding it looks like
+                               // unclosed html and we call this method recursively
+                               // on attributes.
                                $attrs = Sanitizer::decodeTagAttributes( $elementMatches[2] );
+                               // Ensure self-closing tags stay self-closing.
+                               $close = substr( $elementMatches[2], -1 ) === '/' ? ' /' : '';
                                $changed = false;
                                foreach ( [ 'title', 'alt' ] as $attrName ) {
                                        if ( !isset( $attrs[$attrName] ) ) {
@@ -419,7 +463,7 @@ class LanguageConverter {
                                }
                                if ( $changed ) {
                                        $element = $elementMatches[1] . Html::expandAttributes( $attrs ) .
-                                               $elementMatches[3];
+                                               $close . $elementMatches[3];
                                }
                        }
                        $literalBlob .= $element . "\000";
@@ -631,29 +675,43 @@ class LanguageConverter {
                $out = '';
                $length = strlen( $text );
                $shouldConvert = !$this->guessVariant( $text, $variant );
-
-               while ( $startPos < $length ) {
-                       $pos = strpos( $text, '-{', $startPos );
-
-                       if ( $pos === false ) {
+               $continue = 1;
+
+               $noScript = '<script.*?>.*?<\/script>(*SKIP)(*FAIL)';
+               $noStyle = '<style.*?>.*?<\/style>(*SKIP)(*FAIL)';
+               // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+               $noHtml = '<(?:[^>=]*+(?>[^>=]*+=\s*+(?:"[^"]*"|\'[^\']*\'|[^\'">\s]*+))*+[^>=]*+>|.*+)(*SKIP)(*FAIL)';
+               // @codingStandardsIgnoreEnd
+               while ( $startPos < $length && $continue ) {
+                       $continue = preg_match(
+                               // Only match -{ outside of html.
+                               "/$noScript|$noStyle|$noHtml|-\{/",
+                               $text,
+                               $m,
+                               PREG_OFFSET_CAPTURE,
+                               $startPos
+                       );
+
+                       if ( !$continue ) {
                                // No more markup, append final segment
                                $fragment = substr( $text, $startPos );
                                $out .= $shouldConvert ? $this->autoConvert( $fragment, $variant ) : $fragment;
                                return $out;
                        }
 
-                       // Markup found
+                       // Offset of the match of the regex pattern.
+                       $pos = $m[0][1];
+
                        // Append initial segment
                        $fragment = substr( $text, $startPos, $pos - $startPos );
                        $out .= $shouldConvert ? $this->autoConvert( $fragment, $variant ) : $fragment;
-
-                       // Advance position
+                       // -{ marker found, not in attribute
+                       // Advance position up to -{ marker.
                        $startPos = $pos;
-
                        // Do recursive conversion
+                       // Note: This passes $startPos by reference, and advances it.
                        $out .= $this->recursiveConvertRule( $text, $variant, $startPos, $depth + 1 );
                }
-
                return $out;
        }
 
index cb59739..594402e 100644 (file)
        "title-invalid-interwiki": "milungucay a kasabelih satangahan yamalyilu la’cus pisaungay i satangahan a milakuid Wiki masasiket.",
        "title-invalid-talk-namespace": "milungucay a kasabelih satangahan nimicaliw hakay inayi’ay a sasukamu belih",
        "title-invalid-characters": "milungucay a kasabelih satangahan yamalyilu la’cusay a tatebanan-nisulitan: \"$1\".",
+       "title-invalid-magic-tilde": "milunguc tu kasabelih satangahan izaw ku la’cusay a kaliwaza masalaing bacu  (<nowiki>~~~</nowiki>)",
        "title-invalid-too-long": "namilungucay a kasabelih satangahan mangasiw, satangahan pisaungay UTF-8 sakababalic a bang gu amana mangasiw $1 {{PLURAL:$1|wyiyincu}}.",
        "title-invalid-leading-colon": "milungucay a kasabelih  satangahan yamalyilu la’cusay a mahaw-bacu i lalingatuan.",
        "perfcachedts": "isasa’ay u saduba’ kalunasulitan, sazikuz misabaluh tuki sa u $1. saduba’ kalunasulitan sayadah sa kapah misuped  {{PLURAL:$4|1 ku heci|$4 ku heci}}.",
        "viewyourtext": "kapah kisu miciwsace atu kopi ilabu’ tina kasabelih <strong> kisu mikawaway-kalumyiti </strong> yuensma-kodo.",
        "namespaceprotected": "inayi’ ku tungus kisu mikawaway-kalumyiti <strong>$1</strong> pangangananay a salaedan a kasabelih.",
        "customcssprotected": "inayi’ ku tungus kisu mikawaway-kalumyiti tina CSS kasabelih, zayhan tina kasabelih yamalyilu ku zuma misaungayay teked a setin.",
+       "customjsprotected": "kisu sa inayi’ ku tungus mikawaway-kalumyiti tina JavaScript kasabelih, zayhan tina kasabelih yamalyilu tu zuma misaungayay nu teked a setin.",
        "mycustomcssprotected": "inayi’ tungus mikawaway-kalumyiti tina CSS kasabelih.",
        "mycustomjsprotected": "inayi’ tungus kisu mikawaway-kalumyiti tina JavaScript kasabelih.",
        "myprivateinfoprotected": "inayi’ tungus kisu mikawaway-kalumyiti cesyun nu misu.",
        "noemail": "misaungayay \"$1\" inayi’ imyiyo(email) puenengan nasulitan.",
        "noemailcreate": "maydih kisu nipabeli cacay kapahay a imyiyo(email) puenengan.",
        "passwordsent": "misaungayay \"$1\" a baluhay mima mapatahkal tu i saayaway a imyiyo(email) puenengan, kapihalhal henay maala tu tigami miliyaw patalabu aca",
+       "blocked-mailpassword": "numisu a IP puenengan malangat tu caay kahasa mikawaway-kalumyiti, satezep tu namakay tini IP puenengan a mima panukasan sasahicaan a mitena’ patahtah.",
        "mailerror": "pabahel imyiyo(email) mungangaw: $1",
        "emailauthenticated": "imyiyo(email) puenengan nu misu malucek tu i $2 $3.",
        "emailnotauthenticated": "imyiyo(email) puenengan mu misu caay henay malucek, cayhenay patigami kisu isasa’ay a sasahicaan a imyiyo(email).",
        "resettokens-no-tokens": "inayi’ ku miliyaw tu setinay a mima-sabuhat.",
        "resettokens-tokens": "sabuhat:",
        "resettokens-token-label": "$1 (ayza sa ku $2)",
+       "resettokens-watchlist-token": "sapiaca tu aazihan [[Special:Watchlist|miazihay a piazihan tu sulit]] Atom/RSS mima-sabuhat",
        "resettokens-done": "maliyaw tu patizeng mima-sabuhat",
        "resettokens-resetbutton": "miliyaw patizeng mapili’ay a mima-sabuhat",
        "bold_sample": "kibetulay a sulit",
        "userpage-userdoesnotexist-view": "misaungayay canghaw \"$1\" caay henay mapangangan.",
        "blocked-notice-logextract": "tina misaungayay malangat tu ayza.\nisasa’ay ku capi demiad malangatay a nasulitan apabeli miazih tu tatenga’ay:",
        "clearyourcache": "<strong> azihen:</strong>izikuzay misuped kisu kanca palawpes saazihay-sakaluk kabilil-miala ngay maazih sabaluhay sumad.\n* <strong>Firefox / Safari:</strong> pecec <em>Shift</em> sa sapecec <em> miliyaw lisimeten </em>, saca pecec <em>Ctrl-F5</em> saca <em>Ctrl-R</em> (Mac sa ku <em>⌘-R</em>) \n* <strong>Google Chrome:</strong> pecec <em>Ctrl-Shift-R</em> (Mac sa ku <em>⌘-Shift-R</em>) \n* <strong>Internet Explorer:</strong> pecec <em>Ctrl</em> sa sapecec <em> miliyaw lisimeten </em>, saca pecec <em>Ctrl-F5</em>\n* <strong>Opera:</strong> taayaw <em> pili’  →  setin </em> (i Mac ku <em>Opera →  setin tu kanamuhan </em>) nazikuzan sa katukuh aca <em> midimut kasikazan & kazahkezan → palawpes azih  kalunasulitan → kabilil-miala tuway a zunga atu tangan </em>",
+       "usercsspreview": "<strong>mipataayaway miazih tunu misu misaungayay CSS kisu ayza, CSS caay henay misuped!</strong>",
        "userjspreview": "strong>imhini pataayaway miazih kisu numisuay misaungayay a JavaScript.\nJavaScript caay henay misuped!</strong>",
        "sitecsspreview": "<strong>imhini kisu ayza i pataayaway miazih tina CSS, CSS caay henay suped!</strong>",
        "sitejspreview": "<strong> mipataayaway miazih tina JavaScript kisu ayza, JavaScript caay henay misuped!</strong>",
        "permissionserrorstext-withaction": "namakay isasaay {{PLURAL:$1|mahicaay}}, inayi’ kisu situngus miteka $2 miteka tuway misaungay:",
        "recreate-moveddeleted-warn": "<strong> patalaw: imahini kisu miliyaw patizeng nasawniay masipuay tu kasabelih. </strong>\n\nkanca kisu mizateng palalid mikawaway-kalumyiti tina bilih haw?\nitini nipabeli masipu atu milimad nasulitan nazipa’an sapiazih tu tatenga’ay",
        "moveddeleted-notice": "kina kasabelih masipu tu.\nisasa nipabeli kina kasabelihay a masipu atu milimad nasulitan nakawawan, taneng miazih tu tatenga’ay.",
+       "moveddeleted-notice-recent": "ahicanaca, tina a kasabelih ayaw sahenay masipu tu (caay sungaliw 24 a tuki).\nisasa’ sa nipabeli tina kasabelih a masipu atu milimad nasulitan nazipa’an sapihica miazih tu tatenga’ay.",
        "log-fulllog": "ciwsace leku nasulitan-nazipa’an",
        "edit-hook-aborted": "mikawaway-kalumyiti masatezep tuway nay Hook.\nzumasatu caay patukil inayi’ amahicahica buhci tu kamu.",
        "edit-gone-missing": "la’cus misabaluh kasabelih.\nkya kasabelih hakay masipu tuway.",
        "deprecated-self-close-category": "pisaungay la’cus Self-closed HTML aazihen-paya a kasabelih",
        "duplicate-args-category": "anu taazihan-mitudung muawaw haw pisaungay misaliyaway a aazihen-sulyang a kasabelih",
        "expensive-parserfunction-category": "pisaungay sayadah eluc sisetyimo katahkalan nu kalisiw a mapulita kasabelih",
+       "post-expand-template-inclusion-warning": "<strong>patalaw:</strong> nicaliwan taazihan mitudung zikuz tabaki adidi’ matabesiw ku pikelec. uzuma taazihan mitudung lacul a caay papisaungay.",
        "post-expand-template-inclusion-category": "nicaliwan taazihan-mitudung mangasiw kelec nu kasabelih",
        "post-expand-template-argument-category": "taazihan-mitudung aazihen-sulyang izaw layad masekipoay a kasabelih",
        "parser-template-loop-warning": "masedap taazihan-mitudung musaliyut: [[$1]]",
        "parser-unstrip-loop-warning": "masedap tu Unstrip masaliyut.",
        "parser-unstrip-recursion-limit": "Unstrip musaliyuliyud mangasiw tu kelec ($1)",
        "converter-manual-rule-error": "imahini palima-saungay kamu miliyaw likec masedap tu mungangaw",
+       "undo-success": "tina mikawaway-kalumyiti kapah tu mapatiku.\nkapikinsa tu zikuz sasutili’ay sapat nu sulit, malucekay tu ukakaiyan kaidihan isu patiku, satu suped tu zikuz misumad tu pahezek mikawaway-kalumyiti patiku.",
        "undo-failure": "nay mikawaway-kalumyiti a sumad izaw sasula’cus,tina mikawaway-kalumyiti caay patiku.",
        "undo-norev": "uyni mikawaway-kalumyiti inayi’ saca masipu tuway, la’cus patiku",
        "undo-nochange": "tina mikawaway-kalumyiti mapatiku tuway.",
        "rev-deleted-user": "(misipu misaungayay a kalungangan tuway)",
        "rev-deleted-event": "(masipu tu nasulitan-nazipa’an nu paazih tu sulit)",
        "rev-deleted-user-contribs": "[misaungayay a kalungangan saca IP puenengan masipu tuway - madimut paanin piazihan-tu-sulit a mikawaway-kalumyiti]",
+       "rev-deleted-no-diff": "zayhan kasabelih u cacay masumad nu ayaway mapa <strong>masipu</strong>, la’cus kisu miciwsace tu kasasizuma.",
        "rev-delundel": "misumad ku maazihay",
        "rev-showdeleted": "paazih",
        "revisiondelete": "masipu/palawpes misipu masumad nu ayaway",
        "revdelete-nooldid-title": "la’cusay a pamutekan masumad nu ayaway",
+       "revdelete-nooldid-text": "inayi’ matuzu’ay kisu amahicahica tu amisaungay tina sasahicaan pamutekan masumad nu ayaway nu ayaway, saca  matuzu’ay sumad inayi’ay, saca kisu mitanam midimut ayza a sumad",
        "revdelete-no-file": "matuzu’ay a tangan inayi’ tu.",
        "revdelete-show-file-submit": "hang",
        "revdelete-selected-text": "mapili’ tuway [[:$2]] tebanay{{PLURAL:$1|cacayay|yadahay}} masumad nu ayaway",
        "rcfilters-filter-logactions-label": "saungay a nasulitan nazipa’an",
        "rcfilters-filter-logactions-description": "mikuwan saungay, patizeng canghaw, misipu kasabelih, patapabaw...",
        "rcfilters-hideminor-conflicts-typeofchange": "izaw ku zumaay misumad nikalahizaan la’cus matuzu’ay mala \"mikilulay\", sisa tina sakacucek nu misapili’ atu isasa’ay a sumad nikalahizaan sakacucek nu misapili’ sasula’cus: $1",
+       "rcfilters-typeofchange-conflicts-hideminor": "tina misumad nikalahizaan sakacucek nu misapili’ atu \"mikilulay mikawaway-kalumyiti\" sakacucek nu misapili’sasula’cus, uzuma misumad nikalahizaan la’cus matuzu’ay ku \"mikilulay\"",
        "rcfilters-filtergroup-lastRevision": "sabaluhay masumad",
        "rcnotefrom": "isasa’ay a {{PLURAL:$5|ku}}nay <strong>$3 $4</strong> a sumad  (sayadah paazih <strong>$1</strong>).",
        "rclistfrom": "paazih nay $3 $2 baluhayay a sumad katukuh ayza",
        "largefileserver": "tina tangan hacica-tabaki mangsiw sefu-kikay setin a mahasaay a subal.",
        "emptyfile": "patapabaway tu tangan nu misu nayay ilabu.\nhakay u tangan a kalungangan mungangaw ku sulitan.\nkapikinsa maydih kisu patapabaw tu nayaay a tangan.",
        "windows-nonascii-filename": "tina Wiki caay midama pisaungay sazumaay bacu a tangan kalungangan.",
+       "fileexists-no-change": "patapabaway a tangan atu ayza baziyong a <strong>[[:$1]]</strong> tada malecad.",
        "file-exists-duplicate": "tina tangan masaliyaw isasa’ay a {{PLURAL:$1|cacay|yadah}} tangan",
        "uploadwarning": "patapabaw patalaw",
        "uploadwarning-text": "pisumad isasa’ay a tangan sapuelac atu mitanam aca.",
        "upload-form-label-infoform-categories": "kakuniza",
        "upload-form-label-infoform-date": "demiad",
        "upload-form-label-own-work-message-generic-local": "milucek tu kaku patapabaway a tangan nu maku maduduc tu ku isasa’ay {{SITENAME}} miedapay a cedang atu sapabeli tu kinli a cedang",
+       "upload-form-label-not-own-work-message-generic-local": "amahica kisu la’cus patubeli mikilul {{SITENAME}} tu amikuwanay-pasayzaay patapabaw tu tangan, kapiedeb tina a sasukamu-liwang zumasatu mitanam tu zuma a sakaluk.",
        "upload-form-label-not-own-work-local-generic-local": "kapah kisu mitanam [[Special:Upload|pataayaw tu kawaw a patapabaw kasabelih]].",
        "upload-form-label-own-work-message-generic-foreign": "matineng tu kaku patapabaw tina tangan tayza i cacay kapulungan suped-sulu, milucek tu kaku miduduc tu nu wiki a cedang atu sapabeli tu kinli a likec.",
        "upload-form-label-not-own-work-message-generic-foreign": "anu makai kisu patubeli miduduc kasasimel suped-sulu a amikuwanay-pasayzaay patapabaw tu tangan,piedeb tina sasukamu-liwang a mitanam tuzumaay a sakaluk.",
        "http-curl-error": "imahini miala URL sa mungangaw: $1",
        "http-bad-status": "miteka HTTP milunguc izaw tu ku munday: $1 $2",
        "upload-curl-error6": "la’cus misiket tu calay-zazan(wanglu) ta URL",
+       "upload-curl-error6-text": "la’cus misiket tu calay-zazan(wanglu) ta matuzu’ay nu URL.\nkapiliyaw miteka mikinsa URL u tatenga’ay tu haw, nika malucekay tu calay-kakacawan(wangcan) kapah ku pisaungay.",
        "upload-curl-error28": "patapabaw mautang",
        "upload-curl-error28-text": "calay-kakacawan(wangcan) mangasiw patukil a tukiay kelec. \npikinsa kya calay-kakacawan(wangcan) malecek saungay haw? pihanhan henay pitaneng aca.\npatahkal nizateng tisuwan kapah kisu i caay makalahay a tuki mitanam misiket tu calay-zazan(wanglu).",
        "license": "sapabeli tu kinli a cedang",
        "wantedpages": "maydihay a kasabelih",
        "wantedpages-badtitle": "kyu i lecapuay a satangahan la’cus: $1",
        "wantedfiles": "maydihay a tangan",
+       "wantedfiletext-cat-noforeign": "isasa’ay a tangan mapasaungay tu uyzasa inayi’ay. tina a dada’ silabas kakaiyan tu, kasabelih sipakabit ilabu’ nika inayi’ay a tangan mapalaylay i [[:$1]]",
        "wantedtemplates": "maydihay a taazihan mitudung",
        "mostlinked": "masasiket sayadahay a kasabelih",
        "mostlinkedcategories": "masasiket sayadahay a kakuniza",
        "log-name-contentmodel": "lacul tatudungen misanga’ misumad nasulitan nazipa’an",
        "log-description-contentmodel": "tina kasabelih pasilsil tu kasabelih lacul tatudungen misanga’ sumad misulit atu pisaungay caay pataayaw tu kawaw tu lacul nu tatudungen misanga’ patizeng tu kasabelih",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|masaungay tuway}} caay pataayaw tu kawaw a lacul tatudungen misanga’ \"$5\" patizeng kasabelih $3",
+       "logentry-contentmodel-change": "$1 {{GENDER:$2|masumad tuway}} kasabelih $3 u lacul tatudungen misanga’ nay \"$4\" ta \"$5\"",
        "logentry-contentmodel-change-revertlink": "patiku",
        "logentry-contentmodel-change-revert": "patiku",
        "protectlogpage": "midiput nasulitan nakawawan",
        "undeleteinvert": "kabelihan mipili’",
        "undeletecomment": "mahicaay:",
        "cannotundelete": "liyad saca hamin a palawpes  misipu mungangaw:\n$1",
+       "undelete-header": "kapiazih tu tatenga’ay [[Special:Log/delete|masipu nasulitan nazipa’an]] palalitemuh tu kawaw capi demiad masipuay kasabelih.",
        "undelete-search-title": "mikilim masipuay a kasabelih",
        "undelete-search-prefix": "paazih kasabelih miteka nay:",
        "undelete-search-submit": "kilim",
        "ip_range_invalid": "la’cusay IP subal.",
        "ip_range_toolarge": "caay mahasa milangat kya taliyuk mangasiw /$1.",
        "proxyblocker": "kutay sefu-kikay milangat-kikay",
+       "sorbsreason": "numisu a IP u puenengan i {{SITENAME}} pisaungayan  DNSBL mapala mawawah midayli  sefu-kikay",
        "softblockrangesreason": "IP puenengan nu misu ($1) inayi’ mahasa paceba pangangan paanin, pipatalabu.",
        "xffblockreason": "IP puenengan nu misu pisaungay X-Forwarded-For satangahan, kisu saca pisaungay nu misu a kutay sefu-kikay malangat tuway.\nmalangatay a mahicaay ku:$1",
        "ipbblocked": "izay kisu malangat, sisa la’cus milangat saca mihulak malangatay a zuma  misaungayay",
        "lockdbsuccesssub": "malahci pamutek sulu nu nasulitan tuway",
        "unlockdbsuccesssub": "misipu pamutek tu sulu nu kalunasulitan tuway",
        "lockdbsuccesstext": "mamutek tu ku nasulitan-sulu. <br />\namana kapawan anu mahemin midiput pahezek [[Special:UnlockDB| mihulak pamutek ]] nasulitan-sulu.",
+       "lockfilenotwritable": "inayi’ tungus suliten nasulitan-sulu pamutek tu tangan.\ncalay-belih(wangyi) sefu-kikay maydih tu tangan a suliten tungus u azihen atu mihulak pamutek nasulitan-sulu.",
        "databaselocked": "pamutek tuway ku kalunasulitan-sulu",
        "databasenotlocked": "caay pamutek henay ku kalunasulitan-sulu",
        "lockedbyandtime": "(nay {{GENDER:$1|$1}} i $2 a $3)",
        "exporttext": "kapah kisu patahkal matuzu’ay kasabelih saca kayadah belih a sulit atu mikawaway-kalumyiti nazipa’an, pisaungay XML kese a tabu.\nuyniyay tangan kapah pacumud ta zuma pisaungay  MediaWiki a Wiki, micaliw [[Special:Import|pacumud kasabelih]].\n\napatahkal kasabelih, isasa’ nu sulit atilad misulit kasabelih pyawti, cacay pyawti pisaungay cacay tusil, zumasatu pipili’ apatahkal ayzaay a sumad yamalyilu haw sacahamin nazipa’an masumad nu ayaway nasulitan, saca patahkal dada’ ayzaay a sumad atu sazikuzay mikawaway-kalumyiti a cesyun。\n\ni sulit atilad kapah tu kisu pisaungay masasiket, tinaku:[[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] dayhiw patahkal kasabelih \"[[{{MediaWiki:Mainpage}}]]\"",
        "exportall": "patahkal sacahamin  kasabelih",
        "exportcuronly": "yamalyilu dada’ ayzaay a sumad nu ayaway, caay yamalyilu lekuay a sumad nu nazipa’an",
+       "exportnohistory": "<strong>piazihen:</strong>zayhan matalaw tu sefu-kikay silahciay munday, mapasatezep tu patahkal ku kasabelih a sacahamin nazipa’an misilit.",
        "exportlistauthors": "yamalyilu paykasabelih lekuay a piazihan-tu-sulit nu paaninay",
        "export-submit": "patahkal",
        "export-addcattext": "pisaungay kakuniza cunusen kasabelih:",
        "import-error-interwiki": "kasabelih \"$1\" kalungangan maliwan hizantu mala hekalay masasiket (interwiki) pisaungay, la’cus pacumud.",
        "import-error-special": "kasabelih \"$1\" tungusay nu caay mahasa kasabelih a sazumaay azihen pangangananay a salaedan, la’cus  pacumud.",
        "import-error-invalid": "kasabelih \"$1\" pacumud tina Wiki a kalungangan la’cus, la’cus pacumud.",
+       "import-error-bad-location": "tina a Wiki a kasabelih \"$1\" caay pidama pisaungay tu lacul tatudungen misanga’ $3, masumad nu ayaway $2 la’cus misuped tayza i kasabelih.",
        "import-options-wrong": "{{PLURAL:$2|mapili’ay}} mungangaw: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "matuzu’ay a limit-kasabelih satangahan la’cus.",
        "import-rootpage-nosubpage": "pangangananay a salaedann \"$1\" a lamit kasabelih caay mahasa sailuc-kasabelih",
        "creditspage": "kasabelih kasakumi nu misayingaay",
        "nocredits": "tina kasabelih inayi’ kasakumi nu misayingaay cesyun.",
        "spamprotectiontitle": "misapili’ a cengse nu babakahen a sulit",
+       "spamprotectiontext": "misulitan a sulit lacul nu misu mapasatezepan misuped tu tatuni’ palatuh misebsebay a cengse, hakay zayhan misu a lacul yamalyilu tu malangat ku hekal masasiket.",
        "spamprotectionmatch": "isasa’ay a lacul mateka’ babakahen a sulit sebseb cengse:$1",
        "spambot_username": "misadimel MediaWiki babakahen a sulit",
        "spam_reverting": "patiku tayza caay yamalyilu $1 masasiket a sabaluhay masumad nu ayaway",
        "autosumm-new": "napatizeng tu kasabelih, lacul ku \"$1\"",
        "autosumm-newblank": "patizeng nayi’ ku cacan a kasabelih",
        "lag-warn-normal": "tina piazihan tu sulit hakay caay paazih tu macapiay demiad $1 {{PLURAL:$1| widi}}labu’ay a sumad.",
+       "lag-warn-high": "nanu nasulitan-sulu patukil mautang, tina piazihan tu sulit hakay caayay paazih capi demiad $1 {{PLURAL:$1|widi}}ilabu’ay misumad.",
        "watchlistedit-normal-title": "miazihay a piazihan tu sulit nu mikawaway-kalumyiti",
        "watchlistedit-normal-legend": "nay miazihay a piazihan-tu-sulit misipu satangahan",
        "watchlistedit-normal-submit": "misipu satangahan",
        "watchlisttools-raw": "mikawaway-kalumyiti saayaway misisip a piazihan-tu-sulit",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1| sasukamu ]])",
        "timezone-local": "itizaay",
+       "duplicate-displaytitle": "<strong>patalaw:</strong> paazih satangahan \"$2\" mitahpu ayaway paazih satangahan \"$1\".",
        "invalid-indicator-name": "<strong> mungangaw:</strong> kasabelih setyitase micuzu’ay tu kawaw a <code>name</code> susin amana inayi’",
        "version": "baziyong",
        "version-extensions": "malacul tu sacunusan a sakaluk",
        "default-skin-not-found": "ayah! kisu i <code dir=\"ltr\">$wgDefaultSkin</code> misetinay a Wiki  pataayaw tu kawaw nuhekalan <code>$1</code>  la’cus pisaungay.\n\nnilacul laylay nu misu kanca yamalyilu isasa’ay a {{PLURAL:$4| nuhekalan}}. piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Manual:Skin_configuration misaungay a cudad: nuhekalan a setin] kya maala hicaen {{PLURAL:$4|miwawah nuhekalan zumasatu misetin pataayaw tu kawaw sulyang}}a cesyun .\n\n$2\n\n; amahica kisu nasawni milacul tuway MediaWiki:\n: hakay kisu ku pisaungay git saca kakilul micaliw yuensma-kodo pisaungay zuma sakaluk milacul, u malecekay tina pulita。pitanam milacul  [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org a nuhekalan dilyikotoling] a liyad nuhekalan pisaungay isasa’ay a sasakawawen:\n:* patasasa’ [https://www.mediawiki.org/wiki/Special:MyLanguage/Download tarball milacul cengse], kya cengse yamalyilu yadahay nuhekalan atu sacunus. kapah kisu kopi atu mizepit i <code>skins/</code>dilyikotoling. \n:* nay [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] patasasa’ tekeday a nuhekalan tarball.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins pisaungay Git patasasa’ nuhekalan].\n: amahica kisu ku MediaWiki a saayaway miteka a misakakawaway,mahizaay kanca caay lawilawnumisuay a git suped-sulu.\n\n; amahica kisu nasawni pacakat MediaWiki:\n: MediaWiki 1.24 atu sasutili’ay baluhayay a baziyong caaytu lunuk miwawah malacul tu nuhekalan (piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery misaungay a cuad: nuhekalan lunuk mikilim]). kapah kisu isasa’ {{PLURAL:$5|silsil}}pazepit i <code>LocalSettings.php</code> ngay miwawah {{PLURAL:$5| sacahamin }} ayzasa malacul tuway a {{PLURAL:$5|nuhekalan}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; amahica kisu nasawni misumad <code>LocalSettings.php</code>:\n: pisaliyaw milucek kisu misulit a nuhekalan  kalungangan mungangaw haw?",
        "default-skin-not-found-no-skins": "ayah! kisu i <code>$wgDefaultSkin</code> misetinay a Wiki pataayaw tu kawaw nuhekalan <code>$1</code> la’cus pisaungay.\n\ncaay milacul kisu amahicahica a nuhekalan.\n\n; amahica kisu nasawni milacul tuway saca pacakat tuway MediaWiki:\n: hakay kisu ku pisaungay git saca kakilul micaliw yuensma-kodo pisaungay zuma sakaluk milacul, u malecekay tina pulita. MediaWiki 1.24 saca sasutili’ay baluhayay a baziyong i angangan suped-sulu caay yamalyilu amahicahica a nuhekalan. pitanam milacul [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org a nuhekalan dilyikotoling] a liyad nuhekalan pisaungay isasa’ay a sasakawawen:\n:* patasasa’ [https://www.mediawiki.org/wiki/Special:MyLanguage/Download tarball milacul cengse], kya cengse yamalyilu yadahay nuhekalan atu sacunus. kapah kisu kopi atu mizepit i <code>skins/</code>dilyikotoling.\n:* nay [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] patasasa’ tekeday a nuhekalan  tarball.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins  pisaungay Git patasasa’ nuhekalan].\n: amahica kisu ku MediaWiki a saayaway miteka a misakakawaway, mahizaay kanca caay lawilawnumisuay a git suped-sulu。piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Skin_configuration misaungay a cudad: nuhekalan setin] ngay maala hicaen miwawah  nuhekalan zumasatu misetin pataayaw tu kawaw sulyang a cesyun.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (mawawah tu)",
+       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>mapasatezep tu</strong>)",
        "mediastatistics": "myiti sausi cesyun",
        "mediastatistics-summary": "patapabawan tu ku tangan nikalahizaan u sausi ku cesyun, tina aazihen cudad dada’ sausi tangan sefu-kikay baluhay baziyong, caay yamalyilu maluman atu masipuay tu baziyong.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 wyiyincu}} ($2; $3%)",
index ac26e83..30b05b7 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "إنشاء مرشح افتراضي",
        "rcfilters-savedqueries-cancel-label": "ألغ",
        "rcfilters-savedqueries-add-new-title": "احفظ إعدادات المرشحات الحالية",
-       "rcfilters-savedqueries-already-saved": "المرشحات محفوظة بالفعل",
+       "rcfilters-savedqueries-already-saved": "هذه المرشحات محفوظة بالفعل. غير إعداداتك لإنشاء مرشح محفوظ جديد.",
        "rcfilters-restore-default-filters": "استرجاع المرشحات الافتراضية",
        "rcfilters-clear-all-filters": "مسح كل المرشحات",
        "rcfilters-show-new-changes": "عرض أحدث التغييرات",
index a7aac85..33550b8 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Стварыць фільтар па змоўчаньні",
        "rcfilters-savedqueries-cancel-label": "Адмяніць",
        "rcfilters-savedqueries-add-new-title": "Захаваць цяперашнія налады фільтру",
-       "rcfilters-savedqueries-already-saved": "Гэтыя фільтры ўжо захаваныя",
+       "rcfilters-savedqueries-already-saved": "Гэтыя фільтры ўжо захаваныя. Зьмяніце вашыя налады, каб стварыць новы захаваны фільтар.",
        "rcfilters-restore-default-filters": "Аднавіць фільтры па змоўчаньні",
        "rcfilters-clear-all-filters": "Ачысьціць усе фільтры",
        "rcfilters-show-new-changes": "Праглядзець найноўшыя зьмены",
        "uploadstash-bad-path-unknown-type": "Невядомы тып «$1».",
        "uploadstash-bad-path-unrecognized-thumb-name": "Невядомая назва мініятуры.",
        "uploadstash-bad-path-no-handler": "Ня знойдзены апрацоўнік для mime-тыпу $1 файлу $2.",
+       "uploadstash-bad-path-bad-format": "Ключ «$1» мае няслушны фармат.",
        "invalid-chunk-offset": "Няслушнае зрушэньне фрагмэнту",
        "img-auth-accessdenied": "Доступ забаронены",
        "img-auth-nopathinfo": "Адсутнічае PATH_INFO.\nВаш сэрвэр не ўстаноўлены на пропуск гэтай інфармацыі.\nМагчма, ён працуе праз CGI і не падтрымлівае img_auth.\nГлядзіце https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 6c39836..b49efdb 100644 (file)
        "action-applychangetags": "আপনার পরিবর্তনগুলোর সাথে ট্যাগ সংযোজন করুন",
        "action-changetags": "নির্দিষ্ট সংস্করণ এবং লগ ভুক্তিগুলিতে যথেচ্ছভাবে ট্যাগ সংযোজন ও অপসারণ করা",
        "action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার",
-       "action-purge": "এই পাতা হালনাগাদ করার",
+       "action-purge": "এই পাতাটি শোধন করুন",
        "nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি",
        "enhancedrc-history": "ইতিহাস",
index ffd8161..80ecf13 100644 (file)
        "rcfilters-filtergroup-lastRevision": "Darreres revisions",
        "rcfilters-filter-lastrevision-label": "Darrera revisió",
        "rcfilters-filter-lastrevision-description": "El canvi més recent a una pàgina.",
-       "rcfilters-filter-previousrevision-label": "Revisions anteriors",
+       "rcfilters-filter-previousrevision-label": "No la darrera revisió",
        "rcfilters-filter-previousrevision-description": "Tots els canvis que no són «la darrera revisió».",
        "rcfilters-filter-excluded": "Exclòs",
        "rcfilters-exclude-button-off": "Exclou els seleccionats",
index 227d2bb..de6d8bd 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Standardfilter erstellen",
        "rcfilters-savedqueries-cancel-label": "Abbrechen",
        "rcfilters-savedqueries-add-new-title": "Aktuelle Filtereinstellungen speichern",
-       "rcfilters-savedqueries-already-saved": "Diese Filter sind bereits gespeichert",
+       "rcfilters-savedqueries-already-saved": "Diese Filter sind bereits gespeichert. Ändere deine Einstellungen, um einen neuen Gespeicherten Filter zu erstellen.",
        "rcfilters-restore-default-filters": "Standardfilter wiederherstellen",
        "rcfilters-clear-all-filters": "Alle Filter löschen",
        "rcfilters-show-new-changes": "Neueste Änderungen ansehen",
index 61ca0b1..175cba5 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Δημιουργία προεπιλεγμένου φίλτρου",
        "rcfilters-savedqueries-cancel-label": "Ακύρωση",
        "rcfilters-savedqueries-add-new-title": "Αποθήκευση τρεχουσών ρυθμίσεων φίλτρων",
+       "rcfilters-savedqueries-already-saved": "Αυτά τα φίλτρα έχουν ήδη αποθηκευτεί. Αλλάξετε τις παραμέτρους για να δημιουργήσετε ένα νέο Αποθηκευμένο Φίλτρο.",
        "rcfilters-restore-default-filters": "Επαναφορά προεπιλεγμένων φίλτρων",
        "rcfilters-clear-all-filters": "Εκκαθάριση όλων των φίλτρων",
        "rcfilters-show-new-changes": "Προβολή νεότερων αλλαγών",
index 9a2dde2..5083bed 100644 (file)
        "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.",
-       "wrongpassword": "Incorrect password entered.\nPlease try again.",
+       "wrongpassword": "Incorrect username or password entered.\nPlease try again.",
        "wrongpasswordempty": "Password entered was blank.\nPlease try again.",
        "passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.",
        "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Create default filter",
        "rcfilters-savedqueries-cancel-label": "Cancel",
        "rcfilters-savedqueries-add-new-title": "Save current filter settings",
-       "rcfilters-savedqueries-already-saved": "These filters are already saved",
+       "rcfilters-savedqueries-already-saved": "These filters are already saved. Change your settings to create a new Saved Filter.",
        "rcfilters-restore-default-filters": "Restore default filters",
        "rcfilters-clear-all-filters": "Clear all filters",
        "rcfilters-show-new-changes": "View newest changes",
index d275c7a..a59a059 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Koosta vaikefilter",
        "rcfilters-savedqueries-cancel-label": "Loobu",
        "rcfilters-savedqueries-add-new-title": "Salvesta filtri praegused sätted",
-       "rcfilters-savedqueries-already-saved": "Need filtrid on juba salvestatud",
+       "rcfilters-savedqueries-already-saved": "Need filtrid on juba salvestatud. Muuda sätteid, et koostada uus salvestatud filter.",
        "rcfilters-restore-default-filters": "Taasta vaikefiltrid",
        "rcfilters-clear-all-filters": "Eemalda kõik filtrid",
        "rcfilters-show-new-changes": "Vaata uusimaid muudatusi",
        "apisandbox-dynamic-parameters-add-placeholder": "Parameetri nimi",
        "apisandbox-dynamic-error-exists": "Parameeter nimega \"$1\" on juba olemas.",
        "apisandbox-deprecated-parameters": "Vananenud parameetrid",
+       "apisandbox-fetch-token": "Hangi luba automaatselt",
+       "apisandbox-submit-invalid-fields-title": "Mõned väljad on vigased",
+       "apisandbox-submit-invalid-fields-message": "Palun paranda märgitud väljad ja proovi uuesti.",
        "apisandbox-results": "Tulemused",
+       "apisandbox-sending-request": "API päringu saatmine...",
+       "apisandbox-loading-results": "API tulemuste laekumine...",
+       "apisandbox-results-error": "API päringu vastuse laadimisel esines tõrge: $1.",
+       "apisandbox-results-login-suppressed": "See päring tehti välja logitud kasutajaga, mida saab kasutada selleks, et hiilida mööda brauseri sama päritolu turvafunktsioonist. Pane tähele, et sellise päringuga ei töötle API liivakast automaatset luba õigesti. Palun sisesta luba käsitsi.",
+       "apisandbox-request-selectformat-label": "Näita päringu andmeid nii:",
+       "apisandbox-request-format-url-label": "URL-päringusõne",
        "apisandbox-request-url-label": "Päringu URL:",
+       "apisandbox-request-json-label": "Päringu JSON:",
        "apisandbox-request-time": "Päringuaeg: {{PLURAL:$1|$1 ms}}",
+       "apisandbox-results-fixtoken": "Paranda luba ja saada uuesti",
+       "apisandbox-results-fixtoken-fail": "$1-luba ei õnnestunud hankida.",
+       "apisandbox-alert-page": "Osa välju sellel leheküljel pole sobivad.",
+       "apisandbox-alert-field": "Selle välja väärtus pole sobiv.",
+       "apisandbox-continue": "Jätka",
+       "apisandbox-continue-clear": "Tühjenda",
+       "apisandbox-continue-help": "\"{{int:apisandbox-continue}}\" [https://www.mediawiki.org/wiki/API:Query#Continuing_queries jätkab] viimase päringuga; \"{{int:apisandbox-continue-clear}}\" tühjendab jätkuparameetrid.",
+       "apisandbox-param-limit": "Sisesta <kbd>max</kbd>, et kasutada ülemmäära.",
+       "apisandbox-multivalue-all-namespaces": "$1 (kõik nimeruumid)",
+       "apisandbox-multivalue-all-values": "$1 (kõik väärtused)",
        "booksources": "Raamatuotsimine",
        "booksources-search-legend": "Raamatuotsimine",
        "booksources-search": "Otsi",
index 2d66603..ee86a94 100644 (file)
        "fileduplicatesearch-noresults": "Tiedostoa nimeltä ”$1” ei löytynyt.",
        "specialpages": "Toimintosivut",
        "specialpages-note-top": "Merkkien selitys",
+       "specialpages-note-restricted": "* Tavalliset toimintosivut.\n* <span class=\"mw-specialpagerestricted\">Rajoitetut toimintosivut.</span>",
        "specialpages-group-maintenance": "Sivujen huoltaminen",
        "specialpages-group-other": "Muut",
        "specialpages-group-login": "Sisäänkirjautuminen ja tunnusten luonti",
        "compare-title-not-exists": "Määrittämääsi sivua ei ole.",
        "compare-revision-not-exists": "Määrittämääsi versiota ei ole.",
        "diff-form": "Eroavaisuudet",
+       "diff-form-oldid": "Vanhan sivuversion tunnistenumero (vapaaehtoinen tieto)",
+       "diff-form-revid": "Eroavan sivuversion tunnistenumero",
        "diff-form-submit": "Näytä muutokset",
        "permanentlink": "Pysyvä linkki",
        "permanentlink-revid": "Versiotunniste",
+       "permanentlink-submit": "Mene sivuversioon",
        "dberr-problems": "Tällä sivustolla on teknisiä ongelmia.",
        "dberr-again": "Odota hetki ja lataa sivu uudelleen.",
        "dberr-info": "(Tietokantaan ei saada yhteyttä: $1)",
        "gotointerwiki-external": "Olet lähdössä {{GRAMMAR:elative|{{SITENAME}}}} toiselle sivustolle [[$2]].\n\n'''[$1 Jatka osoitteeseen $1]'''",
        "undelete-cantedit": "Et voi palauttaa tätä sivua, koska sinulla ei ole oikeutta muokata tätä sivua.",
        "undelete-cantcreate": "Et voi palauttaa tätä sivua, koska tällä nimellä ei ole olemassaolevaa sivua eikä sinulla ole oikeutta luoda tätä sivua.",
+       "pagedata-title": "Sivudata",
        "pagedata-bad-title": "Virheellinen otsikko: $1."
 }
index 6f3a711..172c0e2 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Créer le filtre par défaut",
        "rcfilters-savedqueries-cancel-label": "Annuler",
        "rcfilters-savedqueries-add-new-title": "Sauvegarder la configuration du filtre courant",
-       "rcfilters-savedqueries-already-saved": "Ces filtres sont déjà enregistrés",
+       "rcfilters-savedqueries-already-saved": "Ces filtres sont déjà enregistrés. Modifiez vos paramètres pour créer un nouveau Filtre enregistré.",
        "rcfilters-restore-default-filters": "Rétablir les filtres par défaut",
        "rcfilters-clear-all-filters": "Effacer tous les filtres",
        "rcfilters-show-new-changes": "Afficher les modifications les plus récentes",
index afe6cb6..493e033 100644 (file)
        "imagelinks": "Failicho vapor",
        "linkstoimage": "{{PLURAL:$1|Hem pan|$1 Him panam}} hea failik {{PLURAL:$1|zoddtta|zoddttat}}",
        "nolinkstoimage": "Hea failik zoddpi panam nant",
-       "sharedupload-desc-here": "Hi fail $1, hachi ani dusreo projectanim haka uzar korunk zata.\nHachem [$2 failichem vivron panan] asleli vivron khala dilea:",
+       "sharedupload-desc-here": "Hi fail $1, hachi ani dusrea prokolpanim haka uzar korunk zata.\nHachem [$2 failichem vivron panan] asleli vivron khala dilea:",
        "upload-disallowed-here": "Tu hea faili voir borounk xokonai",
        "filedelete-otherreason": "Dusrem/aniki karon:",
        "randompage": "Khoincheim adlem modlem pan",
index 2472d85..c691277 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "יצירת מסנן התחלתי",
        "rcfilters-savedqueries-cancel-label": "ביטול",
        "rcfilters-savedqueries-add-new-title": "שמירת הגדרות המסננים הנוכחיות",
-       "rcfilters-savedqueries-already-saved": "מסננים אלה כבר נשמרו",
+       "rcfilters-savedqueries-already-saved": "מסננים אלה כבר נשמרו. באפשרותך לשנות את ההגדרות שלך כדי ליצור ולשמור מסנן חדש.",
        "rcfilters-restore-default-filters": "שחזור למסנני ברירת המחדל",
        "rcfilters-clear-all-filters": "מחיקת כל המסננים",
        "rcfilters-show-new-changes": "הצגת השינויים החדשים ביותר",
index 6584a3d..7744632 100644 (file)
        "rcfilters-restore-default-filters": "Alapértelmezett szűrők visszaállítása",
        "rcfilters-clear-all-filters": "Összes szűrő kikapcsolása",
        "rcfilters-show-new-changes": "Legfrissebb változtatások megtekintése",
-       "rcfilters-search-placeholder": "Friss változtatások szűrése (böngéssz vagy kezdj el gépelni)",
+       "rcfilters-search-placeholder": "Változtatások szűrése (használd a menüt vagy keress szűrőkre)",
        "rcfilters-invalid-filter": "Érvénytelen szűrő",
        "rcfilters-empty-filter": "Nincs aktív szűrő. Minden közreműködés látható.",
        "rcfilters-filterlist-title": "Szűrők",
index 4b6a868..4972ff4 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Crea filtro predefinito",
        "rcfilters-savedqueries-cancel-label": "Annulla",
        "rcfilters-savedqueries-add-new-title": "Salva le impostazioni attuali del filtro",
-       "rcfilters-savedqueries-already-saved": "Questi filtri sono già salvati",
+       "rcfilters-savedqueries-already-saved": "Questi filtri sono già salvati. Modifica le impostazioni per creare un nuovo filtro salvato.",
        "rcfilters-restore-default-filters": "Ripristina i filtri predefiniti",
        "rcfilters-clear-all-filters": "Pulisci tutti i filtri",
        "rcfilters-show-new-changes": "Visualizza le modifiche più recenti",
index 7cae851..0a9e9ea 100644 (file)
        "welcomecreation-msg": "Akun panjenengan wis kacipta. Aja lali nata konfigurasi [[Special:Preferences|preferensi {{SITENAME}}]] panjenengan.",
        "yourname": "Jeneng panganggo:",
        "userlogin-yourname": "Jeneng panganggo",
-       "userlogin-yourname-ph": "Isinen jeneng panganggoné panjenengan",
+       "userlogin-yourname-ph": "Isi jeneng-panganggo panjenengan",
        "createacct-another-username-ph": "Isi jeneng panganggo",
        "yourpassword": "Tembung wadi:",
        "userlogin-yourpassword": "Tembung wadi",
        "createacct-yourpassword-ph": "Isi tembung wadi",
        "yourpasswordagain": "Tik manèh tembung wadiné:",
        "createacct-yourpasswordagain": "Konfirmasi tembung wadi",
-       "createacct-yourpasswordagain-ph": "Lebokaké manèh tembung wadiné",
+       "createacct-yourpasswordagain-ph": "Isi manèh tembung wadi mau",
        "userlogin-remembermypassword": "Gawé supaya panggah mlebu log",
        "userlogin-signwithsecure": "Nganggo koneksi aman",
        "cannotlogin-title": "Ora bisa mlebu log",
        "userlogin-createanother": "Gawé akun liya",
        "createacct-emailrequired": "Alamat layang-èl",
        "createacct-emailoptional": "Alamat layang-èl (manasuka)",
-       "createacct-email-ph": "Isinen layang-èlé panjenengan",
+       "createacct-email-ph": "Isi layang-èl panjenengan",
        "createacct-another-email-ph": "Isi alamat layang-èl",
-       "createaccountmail": "Anggonen tembung wadi sembarang sauntara lan kirimen iku menyang alamat layang-èl sing dikarepaké",
+       "createaccountmail": "Nganggo tembung wadi sauntara sing dikirimaké menyang alamat layang-èl",
        "createacct-realname": "Jeneng asli (manasuka)",
        "createacct-reason": "Alesan",
        "createacct-reason-ph": "Alesané panjenengan nggawé akun liya",
-       "createacct-submit": "Gawé akuné panjenengan",
+       "createacct-submit": "Gawé akun panjenengan",
        "createacct-another-submit": "Gawé akun",
        "createacct-continue-submit": "Banjuraké gawé akun",
        "createacct-another-continue-submit": "Banjuraké gawé akun",
        "nouserspecified": "Panjenengan kudu milih jeneng panganggo.",
        "login-userblocked": "Panganggo iki pinalangan. Ora kena mbelu.",
        "wrongpassword": "Tembung wadi sing diisèkaké salah.\nMangga jajalen manèh.",
-       "wrongpasswordempty": "Panjenengan ora milih tembung sandhi. Mangga dicoba manèh.",
+       "wrongpasswordempty": "Tembung wadi kosong.\nJajalen manèh.",
        "passwordtooshort": "Tembung sesinglon paling sethithik cacahé {{PLURAL:$1|1 aksara|$1 aksara}}.",
        "passwordtoolong": "Tembung wadi ora kena munjuli {{PLURAL:$1|1 pralambang|$1 pralambang}}.",
        "passwordtoopopular": "Tembung wadi sing wis kaprah ora kena dianggo. Mangga pilih tembung wadi liya sing mbédani.",
        "password-login-forbidden": "Panganggoning jeneng panganggo lan tembung wadi iki dilarang.",
        "mailmypassword": "Balèni gawé tembung wadi",
        "passwordremindertitle": "Tembung wadi sauntara kanggo {{SITENAME}}",
-       "passwordremindertext": "Ana wong (mbokmanawa panjenengan dhéwé, saka alamat IP $1) nyuwun supaya dikirimi tembung sandhi anyar kanggo {{SITENAME}} ($4). Tembung sandi sawetara kanggo panganggo \"$2\" wis digawé lan saiki \"$3\". Yèn panjenengan pancèn nggayuh iki, mangga énggal mlebu log lan ngganti tembung sandi saiki.\nTembung sandi sawetara mau bakal kadaluwarsa ing {{PLURAL:$5|sadina|$5 dina}}.\nYèn wong liya sing nglakoni panyuwunan iki, utawa panjenengan éling tembung sandi panjenengan, lan ora kepéngin ngowahi, panjenengan ora usah nggubris pesen iki lan bisa tetep nganggo tembung sandi lawas.",
+       "passwordremindertext": "Ana wong (mbokmanawa panjenengan dhéwé, saka alamat IP $1) nyuwun supaya dikirimi tembung wadi anyar kanggo {{SITENAME}} ($4). Tembung wadi sawetara kanggo panganggo \"$2\" wis digawé lan saiki \"$3\". Yèn panjenengan pancèn nggayuh iki, mangga énggal mlebu log lan ngganti tembung wadi saiki.\nTembung wadi sawetara mau bakal kadaluwarsa ing {{PLURAL:$5|sadina|$5 dina}}.\nYèn wong liya sing nglakoni panyuwunan iki, utawa panjenengan éling tembung wadi panjenengan, lan ora kepéngin ngowahi, panjenengan ora usah nggubris pesen iki lan bisa tetep nganggo tembung wadi lawas.",
        "noemail": "Ora ana alamat layang-èl sing kacathet tumrap ing panganggo \"$1\".",
        "noemailcreate": "Panjenengan kudu maringi alamat e-mail sing absah",
        "passwordsent": "Tembung sandi anyar wis dikirim menyang alamat layang èlèktronik tumrap \"$1\". \nMangga mlebu log manèh sawisé panjenengan nampa iku.",
        "blocked-mailpassword": "Alamat IP-né panjenengan diblokir saka mbesut. Kanggo ngéndhani tumindak salah-guna, ora diparengaké nganggo pamulihan tembung wadi saka alamat IP iki.",
        "eauthentsent": "Layang-èl konfirmasi wis dikirim menyang alamat layang-èl sing diisèkaké. Sadurungé ana layang-èl liyané sing dikirim menyang akun iku, panjenengan kudu nuruti arahan ana ing layang-èl iku saperlu ngonfirmasi yèn akun iku pancèn duwèké panjenengan.",
-       "throttled-mailpassword": "Layang kanggo mbalèkaké tembung sandhi wis dikirim sasuwené ing {{PLURAL:$1|jam|$1 jam}}.\nKanggo nyegah ananing tumindhak culika, namung sak layang kanggo mbalèkaké tembung sandhi sing bakal dikirim sasuwéné ing {{PLURAL:$1|jam|$1 jam}}.",
+       "throttled-mailpassword": "Layang kanggo mbalèkaké tembung wadi wis dikirim sasuwené ing {{PLURAL:$1|jam|$1 jam}}.\nKanggo nyegah ananing tumindhak culika, namung sak layang kanggo mbalèkaké tembung wadi sing bakal dikirim sasuwéné ing {{PLURAL:$1|jam|$1 jam}}.",
        "mailerror": "Masalah pangirim layang: $1",
        "acct_creation_throttle_hit": "Para neneka menyang wiki iki sing nganggo alamat IP-né panjenengan wis gawé {{PLURAL:$1|akun cacah 1|akun cacah $1}} sajeroné $2 pungkasan, sing cacahé nyandhak cacah maksimum sing diidinaké.\nTemahané, para neneka sing nganggo alamat IP iki ora bisa gawé akun manèh sauntara iki.",
        "emailauthenticated": "Alamat layang-èlé panjenengan wis dikonfirmasi ing tanggal $2 pukul $3.",
        "resetpass_header": "Ganti tembung wadining akun",
        "oldpassword": "Tembung wadi lawas:",
        "newpassword": "Tembung wadi anyar:",
-       "retypenew": "Tik manèh tembung wadi anyaré:",
+       "retypenew": "Isi manèh tembung wadi anyaré:",
        "resetpass_submit": "Setèl tembung wadi lan mlebu log",
        "changepassword-success": "Tembung wadiné panjenengan kasil diowah!",
        "changepassword-throttled": "Panjenengan wis kakèhan njajal mlebu log.\nTulung nunggu dhisik $1 sadurungé njajal manèh.",
        "resetpass-temp-password": "Tembung wadi sauntara:",
        "resetpass-abort-generic": "Ngowahi tembung wadi kawurungaké déning èkstènsi.",
        "passwordreset": "Balèni gawé tembung wadi",
-       "passwordreset-text-one": "Lengkapana formulir iki kanggo nampa tembung sandhi sementara lewat layang elektronik.",
-       "passwordreset-text-many": "{{PLURAL:$1|Isinen salah sijine kotak ing ngisor iki kanggo nampa tembung sandhi sementara lewat layang elektronik.}}",
+       "passwordreset-text-one": "Isi formulir iki kanthi jangkep kanggo nampa tembung wadi sauntara lumantar layang-èl.",
+       "passwordreset-text-many": "{{PLURAL:$1|Isi salah siji babagan ing ngisor iki supaya bisa nampa tembung wadi sauntara lumantar layang-èl.}}",
        "passwordreset-disabled": "Setèl ulang tembung wadi dipatèni ing wiki iki.",
        "passwordreset-emaildisabled": "Fitur layang elektronik wis dipateni ing wiki iki.",
        "passwordreset-username": "Jeneng panganggo:",
        "passwordreset-email": "Alamat layang-èl:",
        "passwordreset-emailtitle": "Rerincèné akun ing {{SITENAME}}",
        "passwordreset-emailtext-ip": "Ana wong (bokmanawa panjenengan, saka alamat IP $1) nyuwun tembung wadiné panjenengan disetèl ulang mungguh ing {{SITENAME}} ($4). {{PLURAL:$3|Akun}} ing ngisor iki ana gayutané karo layang-èl iki:\n\n$2\n\n{{PLURAL:$3|Tembung wadi sauntara iki}} bakal kadaluwarsa sawisé {{PLURAL:$5|sadina|$5 dina}}.\nPanjenengan kudu mlebu log lan milih tembung wadi anyar saiki. Yèn wong liya sing nyuwun iki, utawa yèn panjenengan pranyata wis kèlingan tembung wadiné panjenengan sing lawas banjur panjenengan ora nedya ngganti, panjenengan bisa nglirwakaké layang iki lan mbanjuraké nganggo tembung wadiné panjenengan sing lawas.",
-       "passwordreset-emailtext-user": "Panganggo $1 seka {{SITENAME}} njaluk ganti tembung sandhiné Sampéyan ana ing {{SITENAME}} ($4). {{PLURAL:$3|Rèkèning|Rèkèning-rèkèning}} ngisor iki magepokan karo padunungané layang èlèktronik iki:\n\n$2\n\n{{PLURAL:$3|Tembung sandhi sawetara iki}} bakal kedaluwarsa ing {{PLURAL:$5|sak dina|$5 dina}}.\nSampéyan kudu mlebu log lan milih siji tembung sandhi anyar saiki. Yèn wong liya sing njaluk iki, utawa yèn Sampéyan jebul wis kèlingan tembung sandhiné sing lawas saéngga ora ana niyat kanggo ngganti, Sampéyan bisa ngejaraké wara-wara iki lan bacutaké nganggo tembung sandhiné lawas Sampéyan.",
+       "passwordreset-emailtext-user": "Panganggo $1 saka {{SITENAME}} nyuwun ganti tembung wadi panjenengan ana ing {{SITENAME}} ($4). {{PLURAL:$3|Akun|Akun-akun}} ngisor iki magepokan karo alamat layang-èl iki:\n\n$2\n\n{{PLURAL:$3|Tembung-wadi-sauntara}} iki bakal kedaluwarsa ing {{PLURAL:$5|sak dina|$5 dina}}.\npanjenengan kudu mlebu log lan milih siji tembung wadi anyar saiki. Yèn wong liya sing njaluk iki, utawa yèn panjenengan jebul wis kèlingan tembung wadi sing lawas saéngga ora ana niyat kanggo ngganti, panjenengan bisa ngejaraké wara-wara iki lan bacutaké nganggo tembung wadi lawas panjenengan.",
        "passwordreset-emailelement": "Jeneng panganggo: \n$1\n\nTembung wadi sauntara: \n$2",
        "passwordreset-emailsentemail": "Yèn layang èlèktronik iki nggayut akuning sampéyan, layang kanggo salin tembung wadi bakal dikirim.",
        "passwordreset-emailsentusername": "Manawa ana alamat layang-èl sing ana gayutané karo jeneng panganggo iki, layang-èl kanggo nyetèl ulang tembung wadi bakal dikirim.",
        "changeemail": "Owah utawa busak alamat layang-èl",
-       "changeemail-header": "Isinen formulir iki saperlu salin alamat layang-èlé panjenengan. Manawa panjenengan péngin ngilangi gegayutané alamat layang-èl saka akuné panjenengan, kosongaké waé babagan layang-èl anyar nalika ngirim formuliré.",
+       "changeemail-header": "Isi formulir iki saperlu salin alamat layang-èl panjenengan. Manawa panjenengan péngin ngilangi gegayutané alamat layang-èl saka akuné panjenengan, kosongaké waé babagan layang-èl anyar nalika ngirim formuliré.",
        "changeemail-no-info": "Sampéyan kudu mlebu log kanggo ngaksès kaca iki langsung.",
        "changeemail-oldemail": "Alamat layang-èl saiki:",
        "changeemail-newemail": "Alamat layang-èl anyar:",
        "changeemail-none": "(ora ana)",
        "changeemail-password": "Sandi {{SITENAME}} panjenengan:",
        "changeemail-submit": "Ganti layang-èl",
-       "changeemail-nochange": "Mangga isinen mawa alamat layang-èl sing anyar tur béda.",
+       "changeemail-nochange": "Mangga isi mawa alamat layang-èl sing anyar tur béda.",
        "resettokens": "Reset token",
        "resettokens-text": "Anda dapat me-reset Token yang memungkinkan akses ke data pribadi tertentu yang terkait dengan akun Anda di sini.\n\nAnda harus melakukannya jika Anda secara tidak sengaja berbagi dengan seseorang atau jika akun Anda telah disusupi.",
        "resettokens-no-tokens": "Ora ana token sing bisa direset.",
        "searchall": "kabèh",
        "showingresults": "Ing ngisor iki dituduhaké {{PLURAL:$1|'''1''' kasil|'''$1''' kasil}}, wiwitané saking #<strong>$2</strong>.",
        "showingresultsinrange": "Nuduhaké nganti {{PLURAL:$1|<strong>1</strong> kasil|<strong>$1</strong> kasil}} sajeroning penthangan #<strong>$2</strong> tekan #<strong>$3</strong>.",
-       "search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> dari <strong>$3</strong>|Asil <strong>$1 - $2</strong> saking <strong>$3</strong>}}",
+       "search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> saka <strong>$3</strong>|Asil <strong>$1 – $2</strong> saka <strong>$3</strong>}}",
        "search-nonefound": "Ora ana kasil sing cocog karo pitakonan (''query'').",
        "search-nonefound-thiswiki": "Ora ana kasil sing jumbuh karo panjalukan ing situs iki.",
        "powersearch-legend": "Panggolèkan sabanjuré (''advance search'')",
        "prefs-help-gender": "Setèlané pilalan iki sipaté manasuka.\nPiranti alusé nganggo ajiné saperlu nyeluk lan nyebut panjenengan tumraping liyan sarana tembung gèndher sing patut sacara paramasastra.\nKaterangan iki bakal kanton marang umum.",
        "email": "Layang-èl",
        "prefs-help-realname": "Jeneng asli ora kudu diisi.\nYèn diisi, jeneng asliné panjenengan bakal kanggo atribusi awit karyané panjenengan.",
-       "prefs-help-email": "Alamat layang èlèktronik sipaté mung pilihan, nanging dibutuhaké kanggo nyetèl ulang tembung sandhi yèn Sampéyan lali.",
+       "prefs-help-email": "Alamat layang-èl sipaté mung pilihan, nanging dibutuhaké kanggo nyetèl ulang tembung wadi yèn panjenengan lali.",
        "prefs-help-email-others": "Sampéyan uga bisa milih kanggo ngidinaké wong liya ngubungi Sampéyan liwat layang èlèktronik sing ana ing kaca panganggo utawa kaca guneman.\nAlamat layang èlèktronik Sampéyan ora dituduhaké nalika wong liya ngubungi Sampéyan.",
        "prefs-help-email-required": "Butuh alamat layang-èl.",
        "prefs-info": "Katerangan pokok",
        "rcfilters-filter-lastrevision-description": "Mung owahan paling anyar marang kacané.",
        "rcfilters-filter-previousrevision-label": "Dudu révisi pungkasan",
        "rcfilters-filter-previousrevision-description": "Kabèh owahan sing dudu \"révisi pungkasan\".",
-       "rcfilters-view-advanced-filters-label": "Saringan lanjutan",
        "rcfilters-view-tags": "Besutan sing tinengeran",
        "rcfilters-view-namespaces-tooltip": "Saring kasilé miturut mandala-arané",
        "rcfilters-view-tags-tooltip": "Saring kasilé nganggo tengering besutan",
        "emailuser-title-notarget": "Kirimi panganggo layang-èl",
        "emailpagetext": "Panjenengan bisa migunakaké formulir ing ngisor kanggo ngirim layang-e marang {{GENDER:$1|panganggo}} iki.\nAlamat layang-e sing panjenengan lebokaké ing [[Special:Preferences|préferèsi panjenengan]] bakal dadi alamat \"Saka\" jroning layang-e kasebut, mula panampa layang-e bakal bisa mbalesi langsung menyang panjenengan.",
        "defemailsubject": "{{SITENAME}} layang èlèktronik saka panganggo \"$1\"",
-       "usermaildisabled": "Layang-èlé panganggo dipatèni",
+       "usermaildisabled": "Layang-èl panganggo dipatèni",
        "usermaildisabledtext": "Sampéyan ora bisa ngirim layang èlèktronik nèng panganggo liya nèng wiki iki",
        "noemailtitle": "Ora ana alamat layang-èl",
        "noemailtext": "Panganggo iki ora mènèhi alamat layang-e sing absah.",
        "emailccme": "Kirimana aku salinan pesenku.",
        "emailccsubject": "Salinan pesen panjenengan kanggo $1: $2",
        "emailsent": "Layang-èl dikirim",
-       "emailsenttext": "Layang-èlé panjenengan wis dikirim.",
+       "emailsenttext": "Layang-èl panjenengan wis dikirim.",
        "emailuserfooter": "Layang-e iki dikirimaké déning $1 marang $2 migunakaké fungsi \"Layangpanganggo\" ing {{SITENAME}}.",
        "usermessage-summary": "Tinggalaké layang sistem.",
        "usermessage-editor": "Pawartaning layang sistem",
        "logentry-newusers-newusers": "Akun panganggo $1 {{GENDER:$2|digawé}}",
        "logentry-newusers-create": "Akun panganggo $1 {{GENDER:$2|digawé}}",
        "logentry-newusers-create2": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1",
-       "logentry-newusers-byemail": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1 lan tembung sandhine dikirim lewat layang elektronik",
+       "logentry-newusers-byemail": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1 lan tembung wadi dikirim lumantar layang-èl",
        "logentry-newusers-autocreate": "Akun panganggo $1 otomatis {{GENDER:$2|digawé}}",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|ngalih}} setèlan rereksan saka $4 dadi $3",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|njabud}} payomané $3",
        "authmanager-authn-not-in-progress": "Otèntifikasi ora lumaku utawa dhata sèsiné ilang. Mangga ambali kanthi miwiti saka pisanan.",
        "authmanager-authn-autocreate-failed": "Gawéan otomatis akun lokal wurung: $1",
        "authmanager-create-disabled": "Gawéan akun dipatèni.",
-       "authmanager-create-from-login": "Saperlu nggawé akuné panjenengan, mangga isinen babagané.",
+       "authmanager-create-from-login": "Saperlu nggawé akun, panjenengan mangga ngisi babagan-babagan ing ngisor iki.",
        "authmanager-authplugin-setpass-bad-domain": "Dhomain ora trep.",
        "authmanager-autocreate-noperm": "Gawéan akun otomatis ora diidinaké.",
        "authmanager-autocreate-exception": "Gawéan akun otomatis sawetara dipatèni amarga masalah sadurungé.",
index 9121f8a..8a586da 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "기본 필터 만들기",
        "rcfilters-savedqueries-cancel-label": "취소",
        "rcfilters-savedqueries-add-new-title": "현재의 필터 설정 저장",
-       "rcfilters-savedqueries-already-saved": "이 필터는 이미 저장되어 있습니다",
+       "rcfilters-savedqueries-already-saved": "이 필터는 이미 저장되어 있습니다. 새로운 저장된 필터를 만들려면 설정을 변경하십시오.",
        "rcfilters-restore-default-filters": "기본 필터 복구",
        "rcfilters-clear-all-filters": "필터 모두 지우기",
        "rcfilters-show-new-changes": "최신 변경사항 보기",
index 2f501a8..159d808 100644 (file)
        "feedback-cancel": "Anular",
        "feedback-message": "Messaje",
        "feedback-subject": "Sujeto",
-       "searchsuggest-search": "Busxca en {{SITENAME}}",
+       "searchsuggest-search": "Buxca en {{SITENAME}}",
        "duration-seconds": "$1{{PLURAL:$1|segundo|segundos}}",
        "duration-minutes": "$1{{PLURAL:$1|minuto|minutos}}",
        "duration-hours": "$1{{PLURAL:$1|ora|oras}}",
index 5e3778e..5003d46 100644 (file)
        "changeemail-none": "(nav)",
        "changeemail-password": "Jūsu {{SITENAME}} parole:",
        "changeemail-submit": "Mainīt e-pastu",
+       "changeemail-nochange": "Lūdzu, ievadi atšķirīgu jauno e-pasta adresi.",
        "resettokens-tokens": "Marķieri:",
        "resettokens-token-label": "$1 (šībrīža vērtība: $2)",
        "bold_sample": "Teksts treknrakstā",
        "undelete-show-file-submit": "Jā",
        "namespace": "Vārdtelpa:",
        "invert": "Izvēlēties pretēji",
+       "tooltip-invert": "Atzīmē šo rūtiņu, lai paslēptu izmaiņas lapās izvēlētajā vārdtelpā (un saistītajā vārdtelpā, ja tā atzīmēts)",
        "namespace_association": "Saistītā vārdtelpa",
        "tooltip-namespace_association": "Atzīmē šo rūtiņu, lai iekļautu diskusijas vai temata vārdtelpu, kas saistīta ar izvēlēto vārdtelpu",
        "blanknamespace": "(Pamatlapa)",
        "exif-disclaimer": "Atruna",
        "exif-contentwarning": "Brīdinājums par saturu",
        "exif-giffilecomment": "GIF faila komentārs",
+       "exif-subjectnewscode": "Temata kods",
+       "exif-scenecode": "IPTC ainas kods",
        "exif-event": "Attēlotais notikums",
        "exif-organisationinimage": "Attēlotā organizācija",
        "exif-personinimage": "Attēlotā persona",
        "tag-filter": "[[Special:Tags|Iezīmju]] filtrs:",
        "tag-filter-submit": "Filtrs",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Iezīmes|Iezīme|Iezīmes}}]]: $2)",
+       "tag-mw-contentmodelchange": "satura modeļa izmaiņa",
        "tags-title": "Iezīmes",
        "tags-intro": "Šajā lapā uzskaitītas iezīmes, ar kurām programmatūra var atzīmēt labojumus, un to nozīme.",
        "tags-tag": "Iezīmes nosaukums",
        "compare-invalid-title": "Norādītais nosaukums nav derīgs.",
        "compare-title-not-exists": "Norādītais nosaukums neeksistē.",
        "compare-revision-not-exists": "Norādītā versija neeksistē.",
+       "diff-form": "Atšķirības",
        "dberr-problems": "Atvainojiet!\nŠai vietnei ir radušās tehniskas problēmas.",
        "dberr-again": "Uzgaidiet dažas minūtes un pārlādējiet šo lapu.",
        "dberr-info": "(Nevar piekļūt datubāzei: $1)",
index bb0bed3..8cd54d1 100644 (file)
        "duplicate-defaultsort": "'''ताकिद:''' डिफॉल्ट सॉर्ट की \"$2\" ओवर्राइड्स अर्लीयर डिफॉल्ट सॉर्ट की \"$1\".",
        "version": "आवृत्ती",
        "version-extensions": "येथे स्थापलेली विस्तारके",
-       "version-skins": "à¤\87à¤\82सà¥\8dà¤\9fà¥\89ल à¤\95à¥\87लà¥\8dया à¤\97à¥\87लà¥\87लà¥\8dया à¤¤à¥\8dवà¤\9aा",
+       "version-skins": "सà¥\8dथापित à¤¤à¥\8dवà¤\9aा (à¤\87à¤\82सà¥\8dà¤\9fà¥\89लà¥\8dड à¤¸à¥\8dà¤\95िनà¥\8dस)",
        "version-specialpages": "विशेष पाने",
-       "version-parserhooks": "पृथकक अंकुश",
+       "version-parserhooks": "पृथकक अंकुश (पार्सर हूक्स)",
        "version-variables": "चल",
        "version-antispam": "उत्पात प्रतिबंधन",
+       "version-api": "एपीआय (API)",
        "version-other": "इतर",
        "version-mediahandlers": "मिडिया हँडलर",
        "version-hooks": "अंकुश",
-       "version-parser-extensiontags": "पृथकक विस्तारीत खूणा",
-       "version-parser-function-hooks": "पृथकक कार्य अंकुश",
+       "version-parser-extensiontags": "पृथकक विस्तारीत खूणा (पार्सर एक्स्टेंशन टॅग्ज)",
+       "version-parser-function-hooks": "पृथकक कार्य अंकुश (पार्सर फंक्शन हूक्स)",
        "version-hook-name": "अंकुश नाव",
        "version-hook-subscribedby": "वर्गणीदार",
        "version-version": "($1)",
        "version-poweredby-translators": "ट्रांसलेटविकि.नेट वरील भाषांतरकार",
        "version-credits-summary": "आम्ही खालील व्यक्तींना, [[Special:Version|मिडियाविकि]]वर त्यांनी दिलेल्या योगदानामुळे, मान्यता देऊ ईच्छितो.",
        "version-license-info": "मिडियाविकि हे  मुक्त संगणक प्रणाली विकि पॅकेज आहे.Free Software Foundation प्रकाशित  GNU General Public परवान्याच्या अटीस अनुसरून तुम्ही त्यात बदल आणि/अथवा त्याचे  पुर्नवितरण  करू शकता.\n\nमिडियाविकि  संगणक प्रणाली उपयुक्त ठरेल या आशेने वितरित केली जात असली तरी;कोणत्याही वितरणास अथवा विशिष्ट उद्देशाकरिता योग्यतेची अगदी कोणतीही अप्रत्यक्ष अथवा उपलक्षित   अथवा  निहित अशा अथवा कोणत्याही प्रकारच्या केवळ  कोणत्याही प्राश्वासनाशिवायच (WITHOUT ANY WARRANTY) उपलब्ध आहे.अधिक माहिती करिता   GNU General Public License पहावे.\n\nतुम्हाला या प्रणाली सोबत [{{SERVER}}{{SCRIPTPATH}}/COPYING  GNU General Public License परवान्याची प्रत] मिळालेली असावयास हवी, तसे नसेल तर,[//www.gnu.org/licenses/old-licenses/gpl-2.0.html  येथे ऑनलाईन वाचा] किंवा the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ला लिहा.",
-       "version-software": "स्थापित संगणक प्रणाली (Installed software)",
+       "version-software": "स्थापित संगणक प्रणाली (इंस्टॉल्ड सॉफ्टवेअर)",
        "version-software-product": "उत्पादन",
        "version-software-version": "आवृत्ती",
-       "version-entrypoints": "à¤\86त à¤¯à¥\87णारà¥\80 à¤¯à¥\82॰à¤\86र॰à¤\8fल",
+       "version-entrypoints": "पà¥\8dरवà¥\87श-बिà¤\82दà¥\82 à¤¯à¥\82.à¤\86र.à¤\8fल.",
        "version-entrypoints-header-entrypoint": "आत येण्याचा मार्ग",
        "version-entrypoints-header-url": "यू॰आर॰एल",
        "version-libraries-library": "ग्रंथालय",
index bf4a9a8..f4e9e97 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Criar filtro padrão",
        "rcfilters-savedqueries-cancel-label": "Cancelar",
        "rcfilters-savedqueries-add-new-title": "Gravar configurações atuais de filtros",
-       "rcfilters-savedqueries-already-saved": "Esses filtros já foram salvos",
+       "rcfilters-savedqueries-already-saved": "Esses filtros já foram salvos. Altere suas configurações para criar um novo Filtro Salvo.",
        "rcfilters-restore-default-filters": "Restaurar filtros padrão",
        "rcfilters-clear-all-filters": "Limpar todos os filtros",
        "rcfilters-show-new-changes": "Veja as novas mudanças",
index 7f9f286..242f631 100644 (file)
        "internalerror": "اندر دی غلطی",
        "internalerror_info": "اندر دی غلطی:$1",
        "filedeleteerror": "مِسَل \"$1\" کوں مٹایا نی ونڄ سڳیا۔",
+       "filenotfound": "مِسَل \"$1\" کوں لبھ نی سڳیا۔",
        "formerror": "رپھڑ: فارم نی بھیج سڳے",
        "cannotdelete-title": "ورقہ\"$1\" نی مٹا سڳدے",
        "badtitle": "بھیڑا عنوان",
        "resetpass_submit": "پاس ورڈ بݨاؤ تے لاگ ان تھیوو",
        "changepassword-success": "تہاݙا پاس ورڈ تبدیل تھی ڳیا!",
        "botpasswords": "بوٹ پاس ورڈ",
+       "botpasswords-existing": "بوٹ دے موجودہ پاسورڈ",
+       "botpasswords-createnew": "بوٹ دا نواں پاسورڈ بݨاؤ",
        "botpasswords-label-appid": "بوٹ ناں:",
        "botpasswords-label-create": "بݨاؤ",
        "botpasswords-label-update": "اپ ݙیٹ",
        "botpasswords-label-resetpassword": "پاس ورڈ تبدیل کرو",
        "botpasswords-label-grants-column": "ݙے ݙتا ڳئے",
        "botpasswords-bad-appid": "\"$1\" بوٹ ناں ٹھیک کائنی۔",
+       "botpasswords-deleted-title": "بوٹ پاسورڈ مٹ ڳیا",
        "resetpass-submit-loggedin": "پاس ورڈ تبدیل کرو",
        "resetpass-submit-cancel": "منسوخ",
        "resetpass-temp-password": "عارضی لنگھݨ لفظ، پاس ورڈ",
index 770ba73..762538a 100644 (file)
        "editundo": "bolaykeun",
        "diff-empty": "(taya bédana)",
        "diff-multi-sameuser": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya pamaké nu sarua henteu ditémbongkeun)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya leuwih ti {{PLURAL:$2|hiji pamaké|$2 pamaké}} teu ditémbongkeun)",
        "diff-multi-manyusers": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya leuwih ti {{PLURAL:$2|pamaké|pamaké}} teu ditémbongkeun)",
        "searchresults": "Hasil maluruh",
        "searchresults-title": "Hasil nyusud \"$1\"",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:lain</strong> $1",
        "rcfilters-exclude-button-off": "Iwalkeun nu dipilih",
        "rcfilters-exclude-button-on": "Teu kaasup nu dipilih",
-       "rcfilters-view-advanced-filters-label": "Panyaringan leuwih jero",
        "rcfilters-view-tags": "Éditan ditandaan",
        "rcfilters-view-namespaces-tooltip": "Saring hasil dumasar ngarangspasi",
        "rcfilters-view-tags-tooltip": "Saring hasil maké tag éditan",
        "rcfilters-view-return-to-default-tooltip": "Balik ka menu panyaringan utama",
+       "rcfilters-view-tags-help-icon-tooltip": "Teuleuman ngeunaan éditan maké tag",
        "rcfilters-liveupdates-button": "Parobahan langsung",
        "rcfilters-liveupdates-button-title-on": "Pareuman parobahan langsung",
        "rcfilters-liveupdates-button-title-off": "Témbongkeun parobahan anyar nalika éta parobahan prung",
        "uploadstash-refresh": "Nyegerken deui daptar berkas",
        "uploadstash-thumbnail": "tempo miniatur",
        "uploadstash-exception": "Teu bisa nyimpen unjalan di panyimpenan ($1): \"$2\".",
+       "uploadstash-bad-path": "Euweuh galur",
+       "uploadstash-bad-path-invalid": "Galur teu sah.",
+       "uploadstash-bad-path-unknown-type": "Jinis teu dipikanyaho \"$1\".",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Ngaran liwatan teu dipakawawuh.",
+       "uploadstash-file-not-found-no-thumb": "Teu bisa nyomot tampilan saliwat.",
+       "uploadstash-no-extension": "Éksténsi nyamos.",
+       "uploadstash-zero-length": "Berkas mangrupa nol panjang.",
        "invalid-chunk-offset": "Opsét potongan teu valid",
        "img-auth-accessdenied": "Aksés ditolak",
        "img-auth-badtitle": "Teu bisa nyieun judul nu valid tina \"$1\".",
        "filehist-comment": "Kamandang",
        "imagelinks": "Pamakéan berkas",
        "linkstoimage": "Kaca ieu  {{PLURAL:$1|numbu|$1 numbu}} ka gambar ieu :",
+       "linkstoimage-more": "Leuwih ti $1 {{PLURAL:$1|kaca nutumbu|kaca nutumbu}} ka ieu berkas.\nBéréndélan di handap némbongkeun {{PLURAL:$1|tutumbu kaca kahiji|$1 tutumbu kaca}} ka ieu berkas hungkul.\n[[Special:WhatLinksHere/$2|Béréndélan lengkepna]] aya.",
        "nolinkstoimage": "Teu aya kaca anu nutumbu ka ieu berkas.",
        "morelinkstoimage": "Témbong [[Special:WhatLinksHere/$1|tutumbu lianna]] ka ieu berkas.",
        "linkstoimage-redirect": "$1 (pangalihan berkas) $2",
        "listusers-blocked": "(diblokir)",
        "activeusers": "Béréndélan pamaké nu getol",
        "activeusers-intro": "Ieu béréndélan kontributor anu geus ngoprék $1 {{PLURAL:$1|poé|poé}} panungtung.",
-       "activeusers-count": "$1 {{PLURAL:$1|aktivitas}} dina {{PLURAL:$3|1 hari|$3 hari}} panungtung",
+       "activeusers-count": "$1 {{PLURAL:$1|aktivitas}} dina {{PLURAL:$3|sapoé|$3 poé}} panungtung",
        "activeusers-from": "Témbongkeun kontributor dimimitian ku:",
        "activeusers-groups": "Témbongkeun pamaké nu kaasup gorombolan:",
        "activeusers-excludegroups": "Samunikeun pamaké nu kaasup gorombolan:",
        "ipb_cant_unblock": "Éror: ID peungpeuk $1 teu kapanggih. Sigana mah geus dibuka.",
        "ip_range_invalid": "Angka IP teu bener.",
        "ip_range_toolarge": "Panteng blok leuwih badag tibatan /$1 teu diheugbaékeun.",
+       "ip_range_toolow": "Panteng UP sacara éféktif teu diidinan.",
        "proxyblocker": "Pameungpeuk proxy",
        "proxyblockreason": "Alamat IP anjeun dipeungpeuk sabab mangrupa proxy muka. Mangga tepungan ''Internet service provider'' atanapi ''tech support'' anjeun, béjakeun masalah serius ieu.",
        "sorbsreason": "Alamat IP anjeun kadaptar salaku ''open proxy'' dina DNSBL anu dipaké ku {{SITENAME}}.",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|mupus}} panangtayungan ti $3",
        "logentry-protect-protect": "$1 {{GENDER:$2|ditangtayungan}} $3 $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|ngamuat}} $3",
+       "logentry-upload-overwrite": "$1 {{GENDER:$2|ngunggah}} $3 vérsi anyar",
        "logentry-upload-revert": "$1 {{GENDER:$2|diunjal}} $3",
        "log-name-managetags": "Log pangokolaan tag",
        "logentry-managetags-create": "$1 {{GENDER:$2|nyieun}} tag \"$4\"",
index fd51e98..b60268e 100644 (file)
@@ -34,7 +34,6 @@
        "underline-always": "Кезээде",
        "underline-never": "Кажан-даа",
        "underline-default": "Кештиң азы веб-браузерниң ниити үнези",
-       "editfont-default": "Веб-браузерниң ниити үнези",
        "sunday": "Улуг-хүн",
        "monday": "Бир дугаар хүн",
        "tuesday": "Ийи дугаар хүн",
@@ -94,7 +93,7 @@
        "hidden-category-category": "Чажыт бөлүктер",
        "category-subcat-count": "{{PLURAL:$2|1=Ук аңгылал чүгле дараазында иштики аңгылалдыг.|Ук аңгылалда бар-ла $2 иштики аңгылалдарның $1 иштики аңгылалы көстүп турар.}}",
        "category-subcat-count-limited": "Ук аңгылалда {{PLURAL:$1|1=бир|$1}} иштики аңгылал бар.",
-       "category-article-count": "{{PLURAL:$2|1=Ук аңгылалда чүгле чаңгыс арын бар.|Ук аңгылалда бар $2 арыннарының аразындан}} |{{PLURAL:$1 арынны көргүскен| $1 арыннарны көргүскен.}}",
+       "category-article-count": "{{PLURAL:$2|Ук аңгылалда чүгле чаңгыс арын бар.|Аңгылалда ниитизи-биле $2 арын бар. Мында чүгле {{PLURAL:$1|арын|$1 арын}} көргүскен}}",
        "category-file-count": "{{PLURAL:$2|1=Ук аңгылал чүгле чаңгыс файлдыг.|Ук аңгылалдың шупту $2 файлдарының аразындан $1 файлын көргүскен.}}",
        "listingcontinuesabbrev": "(уланчы)",
        "noindex-category": "Индекстелбес арынар",
        "anontalk": "Бо ИП-адрестиң чугаазы",
        "navigation": "Навигация",
        "and": "&#32;болгаш",
-       "qbfind": "Дилээри",
-       "qbbrowse": "Каралаары",
-       "qbedit": "Өскертири",
-       "qbpageoptions": "Бо арын",
-       "qbmyoptions": "Мээң арыннарым",
        "faq": "Бо-ла салыр айтырыглар (БлСА)",
-       "faqpage": "Project:БлСА",
        "actions": "Кылыглар",
        "namespaces": "Аттар делгемнери",
        "variants": "Янзы-хевирлери",
        "edit": "Эдер",
        "create": "Чогаадыры",
        "create-local": "Кызыы тайылбыр немээр",
-       "editthispage": "Бо арынны өскертири",
-       "create-this-page": "Бо арынны чогаадыры",
        "delete": "Ыраары",
-       "deletethispage": "Бо арынны ырадыры",
        "undelete_short": "$1 {{PLURAL:$1|1=эдигни|эдиглерни}} катап үндүрери",
        "viewdeleted_short": "{{PLURAL:$1|1=Бир ыраткан өскерлиишкинни|$1 ыраткан өскерлиишкиннерни}} көөрү",
        "protect": "Камгалаары",
        "protect_change": "өскертири",
-       "protectthispage": "Бо арынны камгалаар",
        "unprotect": "Камгалалды өскертири",
-       "unprotectthispage": "Бо арынның камгалалын өскертири",
        "newpage": "Чаа арын",
-       "talkpage": "Бо арын дугайында чугаалажыры",
        "talkpagelinktext": "Чугаалажып сайгарар",
        "specialpage": "Тускай арын",
        "personaltools": "Хууда херекселдер",
-       "articlepage": "Допчу арынны көөрү",
        "talk": "Сайгарылга",
        "views": "Көрүлделер",
        "toolbox": "Херекселдер",
-       "userpage": "Ажыглакчының арынын көөрү",
-       "projectpage": "Төлевилелдиң арынын көөрү",
        "imagepage": "Файлдың арынын көөрү",
        "mediawikipage": "Чагаа арынын көөрү",
        "templatepage": "Майык арынын көөрү",
        "whatlinkshere-filters": "Шүүрлер",
        "block": "Ажыглакчыны кызыгаарлаары",
        "blockip": "Ажыглакчыны кызыгаарлаары",
-       "blockip-legend": "Ажыглакчыны кызыгаарлаары",
        "ipaddressorusername": "ИП-адрес азы aжыглaкчының aды",
        "ipbreason": "Чылдагаан:",
        "ipbsubmit": "Бо ажыглакчыны кызыгаарлаары",
        "namespacesall": "шупту",
        "monthsall": "шупту",
        "recreate": "Катап чогаадыры",
+       "confirm-purge-title": "Ук арында кешти аштаар",
        "confirm_purge_button": "Чөп",
+       "confirm-purge-top": "Ук арында кешти аштаар бе?",
+       "confirm-purge-bottom": "Арында кешти аштаза, аңаа арынның сөөлгү хевири көстүр.",
        "confirm-watch-button": "Чөп",
        "confirm-unwatch-button": "Чөп",
        "imgmultipageprev": "← эрткен арын",
index 9ec4cce..27fd78b 100644 (file)
@@ -33,7 +33,8 @@
                        "Junaid Ahmad",
                        "Abuaneeqa",
                        "Saraiki",
-                       "BukhariSaeed"
+                       "BukhariSaeed",
+                       "Zainab Meher"
                ]
        },
        "tog-underline": "ربط کی خط کشیدگی:",
        "rcfilters-savedqueries-apply-and-setdefault-label": "طے شدہ فلٹر بنائیں",
        "rcfilters-savedqueries-cancel-label": "منسوخ کریں",
        "rcfilters-savedqueries-add-new-title": "فلٹر کی موجودہ ترتیبات محفوظ کریں",
+       "rcfilters-savedqueries-already-saved": "یہ فلٹرز پھلے سے محفوظ کر دیۓ گۓ ہیں۔نۓ فحفوظ شدہ فلٹرز بنانے کے لیۓاپنی ترکیب تبدیل کریں۔",
        "rcfilters-restore-default-filters": "طے شدہ فلٹر بحال کریں",
        "rcfilters-clear-all-filters": "تمام فلٹروں کو ہٹائیں",
        "rcfilters-show-new-changes": "تازہ ترین تبدیلیاں دیکھیں",
-       "rcfilters-search-placeholder": "حاÙ\84Û\8cÛ\81 ØªØ¨Ø¯Û\8cÙ\84Û\8cÙ\88Úº Ú©Ø§ Ù\81Ù\84ٹر (تÙ\84اش Û\8cا ØªØ­Ø±Û\8cر کریں)",
+       "rcfilters-search-placeholder": "تبدÛ\8cÙ\84Û\8cÙ\88Úº Ú©Ù\88 Ù\81Ù\84ٹر Ú©Ø±Û\8cÚº (Ù\81Ù\84ٹر Ú©Ø§ Ù\86اÙ\85 Ø¬Ø§Ù\86Ù\86Û\92 Ú©Û\92 Ù\84Û\8cÛ\92 Ù\85Û\8cÙ\86Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ú©Ø±Û\8cÚº Û\8cا ØªÙ\84اش کریں)",
        "rcfilters-invalid-filter": "نادرست فلٹر",
        "rcfilters-empty-filter": "کوئی فلٹر فعال نہیں ہے چنانچہ تمام شراکتیں دکھائی جا رہی ہیں۔",
        "rcfilters-filterlist-title": "فلٹروں کی فہرست",
index 5199b07..958ac07 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "创建默认过滤器",
        "rcfilters-savedqueries-cancel-label": "取消",
        "rcfilters-savedqueries-add-new-title": "保存当前过滤器设置",
-       "rcfilters-savedqueries-already-saved": "这些过滤器已保存",
+       "rcfilters-savedqueries-already-saved": "这些过滤器已保存。更改您的设置以创建新的保存过滤器。",
        "rcfilters-restore-default-filters": "恢复默认过滤器",
        "rcfilters-clear-all-filters": "清空所有过滤器",
        "rcfilters-show-new-changes": "显示最新更改",
index ba66c76..f8f5dcd 100755 (executable)
@@ -170,6 +170,26 @@ class UpdateMediaWiki extends Maintenance {
 
                $time1 = microtime( true );
 
+               $badPhpUnit = dirname( __DIR__ ) . '/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php';
+               if ( file_exists( $badPhpUnit ) ) {
+                       // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+                       // Bad versions of the file are:
+                       // https://raw.githubusercontent.com/sebastianbergmann/phpunit/c820f915bfae34e5a836f94967a2a5ea5ef34f21/src/Util/PHP/eval-stdin.php
+                       // https://raw.githubusercontent.com/sebastianbergmann/phpunit/3aaddb1c5bd9b9b8d070b4cf120e71c36fd08412/src/Util/PHP/eval-stdin.php
+                       // @codingStandardsIgnoreEnd
+                       $md5 = md5_file( $badPhpUnit );
+                       if ( $md5 === '120ac49800671dc383b6f3709c25c099'
+                               || $md5 === '28af792cb38fc9a1b236b91c1aad2876'
+                       ) {
+                               $success = unlink( $badPhpUnit );
+                               if ( $success ) {
+                                       $this->output( "Removed PHPUnit eval-stdin.php to protect against CVE-2017-9841\n" );
+                               } else {
+                                       $this->error( "Unable to remove $badPhpUnit, you should manually. See CVE-2017-9841" );
+                               }
+                       }
+               }
+
                $shared = $this->hasOption( 'doshared' );
 
                $updates = [ 'core', 'extensions' ];
index 34b0836..479ade0 100644 (file)
@@ -2083,7 +2083,7 @@ return [
        'mediawiki.special.pagesWithProp' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
        ],
-       'mediawiki.special.preferences' => [
+       'mediawiki.special.preferences.ooui' => [
                'scripts' => [
                        'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
@@ -2100,9 +2100,11 @@ return [
                        'mediawiki.language',
                        'mediawiki.confirmCloseWindow',
                        'mediawiki.notification.convertmessagebox',
+                       'oojs-ui-widgets',
+                       'mediawiki.widgets.SelectWithInputWidget',
                ],
        ],
-       'mediawiki.special.preferences.styles' => [
+       'mediawiki.special.preferences.styles.ooui' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
        ],
        'mediawiki.special.recentchanges' => [
index 6931c7d..58e00f9 100644 (file)
@@ -69,16 +69,16 @@ a.stub {
 }
 
 /* Expand URLs for printing */
-.mw-body-content a.external.text:after,
-.mw-body-content a.external.autonumber:after {
+.mw-parser-output a.external.text:after,
+.mw-parser-output a.external.autonumber:after {
        content: ' (' attr( href ) ')';
        word-break: break-all;
        word-wrap: break-word;
 }
 
 /* Expand protocol-relative URLs for printing */
-.mw-body-content a.external.text[ href^='//' ]:after,
-.mw-body-content a.external.autonumber[ href^='//' ]:after {
+.mw-parser-output a.external.text[ href^='//' ]:after,
+.mw-parser-output a.external.autonumber[ href^='//' ]:after {
        content: ' (https:' attr( href ) ')';
 }
 
index 7b2d711..596b0d6 100644 (file)
@@ -220,28 +220,6 @@ table.toc td {
        font-size: larger;
 }
 
-/* preference page with js-genrated toc */
-#preftoc {
-       float: left;
-       margin: 1em 1em 1em 1em;
-       width: 13em;
-}
-
-#preftoc li {
-       border: 1px solid #fff;
-}
-
-#preftoc li.selected {
-       background-color: #f9f9f9;
-       border: 1px dashed #aaa;
-}
-
-#preftoc a,
-#preftoc a:active {
-       display: block;
-       color: #005189;
-}
-
 .mw-prefs-buttons {
        clear: left;
        float: left;
index d519648..5386291 100644 (file)
                        this.changesListModel.update(
                                pieces.changes,
                                pieces.fieldset,
-                               pieces.noResultsDetails === 'NO_RESULTS_TIMEOUT',
+                               pieces.noResultsDetails,
                                true // We're using existing DOM elements
                        );
                }
index 2a64aa3..d82ffe0 100644 (file)
@@ -9,7 +9,7 @@
  * compatibility ( browsers able to understand gradient syntax support also SVG ).
  * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
 
-.mw-body-content a.external,
+.mw-parser-output a.external,
 .link-https {
        background: url( images/external-ltr.png ) center right no-repeat;
        /* @embed */
@@ -19,7 +19,7 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='mailto:' ],
+.mw-parser-output a.external[ href^='mailto:' ],
 .link-mailto {
        background: url( images/mail.png ) center right no-repeat;
        /* @embed */
@@ -27,7 +27,7 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='ftp://' ],
+.mw-parser-output a.external[ href^='ftp://' ],
 .link-ftp {
        background: url( images/ftp-ltr.png ) center right no-repeat;
        /* @embed */
@@ -35,8 +35,8 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='irc://' ],
-.mw-body-content a.external[ href^='ircs://' ],
+.mw-parser-output a.external[ href^='irc://' ],
+.mw-parser-output a.external[ href^='ircs://' ],
 .link-irc {
        background: url( images/chat-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.ogg' ],
-.mw-body-content a.external[ href$='.OGG' ],
-.mw-body-content a.external[ href$='.mid' ],
-.mw-body-content a.external[ href$='.MID' ],
-.mw-body-content a.external[ href$='.midi' ],
-.mw-body-content a.external[ href$='.MIDI' ],
-.mw-body-content a.external[ href$='.mp3' ],
-.mw-body-content a.external[ href$='.MP3' ],
-.mw-body-content a.external[ href$='.wav' ],
-.mw-body-content a.external[ href$='.WAV' ],
-.mw-body-content a.external[ href$='.wma' ],
-.mw-body-content a.external[ href$='.WMA' ],
+.mw-parser-output a.external[ href$='.ogg' ],
+.mw-parser-output a.external[ href$='.OGG' ],
+.mw-parser-output a.external[ href$='.mid' ],
+.mw-parser-output a.external[ href$='.MID' ],
+.mw-parser-output a.external[ href$='.midi' ],
+.mw-parser-output a.external[ href$='.MIDI' ],
+.mw-parser-output a.external[ href$='.mp3' ],
+.mw-parser-output a.external[ href$='.MP3' ],
+.mw-parser-output a.external[ href$='.wav' ],
+.mw-parser-output a.external[ href$='.WAV' ],
+.mw-parser-output a.external[ href$='.wma' ],
+.mw-parser-output a.external[ href$='.WMA' ],
 .link-audio {
        background: url( images/audio-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.ogm' ],
-.mw-body-content a.external[ href$='.OGM' ],
-.mw-body-content a.external[ href$='.avi' ],
-.mw-body-content a.external[ href$='.AVI' ],
-.mw-body-content a.external[ href$='.mpeg' ],
-.mw-body-content a.external[ href$='.MPEG' ],
-.mw-body-content a.external[ href$='.mpg' ],
-.mw-body-content a.external[ href$='.MPG' ],
+.mw-parser-output a.external[ href$='.ogm' ],
+.mw-parser-output a.external[ href$='.OGM' ],
+.mw-parser-output a.external[ href$='.avi' ],
+.mw-parser-output a.external[ href$='.AVI' ],
+.mw-parser-output a.external[ href$='.mpeg' ],
+.mw-parser-output a.external[ href$='.MPEG' ],
+.mw-parser-output a.external[ href$='.mpg' ],
+.mw-parser-output a.external[ href$='.MPG' ],
 .link-video {
        background: url( images/video.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.pdf' ],
-.mw-body-content a.external[ href$='.PDF' ],
-.mw-body-content a.external[ href*='.pdf#' ],
-.mw-body-content a.external[ href*='.PDF#' ],
-.mw-body-content a.external[ href*='.pdf?' ],
-.mw-body-content a.external[ href*='.PDF?' ],
+.mw-parser-output a.external[ href$='.pdf' ],
+.mw-parser-output a.external[ href$='.PDF' ],
+.mw-parser-output a.external[ href*='.pdf#' ],
+.mw-parser-output a.external[ href*='.PDF#' ],
+.mw-parser-output a.external[ href*='.pdf?' ],
+.mw-parser-output a.external[ href*='.PDF?' ],
 .link-document {
        background: url( images/document-ltr.png ) center right no-repeat;
        /* @embed */
 }
 
 /* Interwiki styling */
-.mw-body-content a.extiw,
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw,
+.mw-parser-output a.extiw:active {
        color: #36b;
 }
 
 /* External link color */
-.mw-body-content a.external {
+.mw-parser-output a.external {
        color: #36b;
 }
index 366c5a9..3599f34 100644 (file)
@@ -53,33 +53,33 @@ a.new:visited,
 }
 
 /* Interwiki Styling */
-.mw-body-content a.extiw,
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw,
+.mw-parser-output a.extiw:active {
        color: #36b;
 }
 
-.mw-body-content a.extiw:visited {
+.mw-parser-output a.extiw:visited {
        color: #636;
 }
 
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw:active {
        color: #b63;
 }
 
 /* External links */
-.mw-body-content a.external {
+.mw-parser-output a.external {
        color: #36b;
 }
 
-.mw-body-content a.external:visited {
+.mw-parser-output a.external:visited {
        color: #636; /* T5112 */
 }
 
-.mw-body-content a.external:active {
+.mw-parser-output a.external:active {
        color: #b63;
 }
 
-.mw-body-content a.external.free {
+.mw-parser-output a.external.free {
        word-wrap: break-word;
 }
 
index 45df37f..fe127eb 100644 (file)
@@ -4,9 +4,11 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var allowCloseWindow;
+               var allowCloseWindow, saveButton, restoreButton;
 
-               // Check if all of the form values are unchanged
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
                function isPrefsChanged() {
                        var inputs = $( '#mw-prefs-form :input[name]' ),
                                input, $input, inputType,
                        return false;
                }
 
+               saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+               restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
                // Disable the button to save preferences unless preferences have changed
                // Check if preferences have been changed before JS has finished loading
                if ( !isPrefsChanged() ) {
-                       $( '#prefcontrol' ).prop( 'disabled', true );
-                       $( '#preferences > fieldset' ).one( 'change keydown mousedown', function () {
-                               $( '#prefcontrol' ).prop( 'disabled', false );
+                       saveButton.setDisabled( true );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).one( 'change keydown mousedown', function () {
+                               saveButton.setDisabled( false );
                        } );
                }
 
                        namespace: 'prefswarning'
                } );
                $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
-               $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
+               restoreButton.on( 'click', function () {
+                       allowCloseWindow.release();
+                       // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                       // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                       location.href = restoreButton.getHref();
+               } );
        } );
 }( mediaWiki, jQuery ) );
index 33b630a..294dcd0 100644 (file)
@@ -1,27 +1,28 @@
 /* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
        border: 1px solid #fde29b;
        background-color: #fdf1d1;
        color: #000;
 }
 /* Authenticated email field has its own class too. Unstyled by default */
 /*
-.mw-email-authenticated .mw-input { }
+.mw-email-authenticated .oo-ui-labelWidget { }
 */
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
 
-#preferences > fieldset table {
-       width: 100%;
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
 }
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
+
+.mw-prefs-buttons {
+       margin-top: 1em;
 }
 
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+#prefcontrol {
+       margin-right: 0.5em;
+}
 
 /*
  * Hide, but keep accessible for screen-readers.
        zoom: 1;
 }
 
-.client-nojs #preftoc {
-       display: none;
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
 }
 
-.client-js #preferences > fieldset {
-       display: none;
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
 }
 
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
-       display: block;
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
 }
index dcfad27..9f1691c 100644 (file)
@@ -3,29 +3,10 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+               var $preferences, tabs, wrapper, previousTab;
 
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
                $preferences = $( '#preferences' );
 
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
                // Make sure the accessibility tip is selectable so that screen reader users take notice,
                // but hide it per default to reduce interface clutter. Also make sure it becomes visible
                // when selected. Similar to jquery.mw-jump
                                } else {
                                        $( this ).css( 'height', 'auto' );
                                }
-                       } ).insertBefore( $preftoc );
+                       } ).prependTo( '#mw-content-text' );
 
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
                        // Handle hash manually to prevent jumping,
                        // therefore save and restore scrollTop to prevent jumping.
                        scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
                        }
                        $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
                }
 
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
                        }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
                        }
-               } );
+               }
 
                // Jump to correct section as indicated by the hash.
                // This function is called onload and onhashchange.
                        }
                }
 
-               // In browsers that support the onhashchange event we will not bind click
-               // handlers and instead let the browser do the default behavior (clicking the
-               // <a href="#.."> will naturally set the hash, handled by onhashchange.
-               // But other things that change the hash will also be caught (e.g. using
+               // Handle other things that change the hash (e.g. using
                // the Back and Forward browser navigation).
-               // Note the special check for IE "compatibility" mode.
-               if ( 'onhashchange' in window &&
-                       ( document.documentMode === undefined || document.documentMode >= 8 )
-               ) {
+               if ( 'onhashchange' in window ) {
                        $( window ).on( 'hashchange', function () {
                                var hash = location.hash;
                                if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                } else if ( hash === '' ) {
                                        switchPrefTab( 'personal', 'noHash' );
                                }
-                       } )
-                               // Run the function immediately to select the proper tab on startup.
-                               .trigger( 'hashchange' );
-               // In older browsers we'll bind a click handler as fallback.
-               // We must not have onhashchange *and* the click handlers, otherwise
-               // the click handler calls switchPrefTab() which sets the hash value,
-               // which triggers onhashchange and calls switchPrefTab() again.
-               } else {
-                       $preftoc.on( 'click', 'li a', function ( e ) {
-                               switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
-                               e.preventDefault();
                        } );
-                       // If we've reloaded the page or followed an open-in-new-window,
-                       // make the selected tab visible.
-                       detectHash();
                }
 
+               // If we've reloaded the page or followed an open-in-new-window,
+               // make the selected tab visible.
+               detectHash();
+
                // Restore the active tab after saving the preferences
                previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
                if ( previousTab ) {
                }
 
                $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       var value = tabs.getCurrentTabPanelName();
                        mw.storage.session.set( 'mwpreferences-prevTab', value );
                } );
 
index 03656ee..7fbcc77 100644 (file)
@@ -4,13 +4,19 @@
 ( function ( mw, $ ) {
        $( function () {
                var
-                       $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+                       timezoneWidget, $localtimeHolder, servertime;
 
                // Timezone functions.
                // Guesses Timezone from browser and updates fields onchange.
 
-               $tzSelect = $( '#mw-input-wptimecorrection' );
-               $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+               try {
+                       timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       timezoneWidget = null;
+               }
+
                $localtimeHolder = $( '#wpLocalTime' );
                servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
 
 
                function updateTimezoneSelection() {
                        var minuteDiff, localTime,
-                               type = $tzSelect.val();
+                               type = timezoneWidget.dropdowninput.getValue();
 
                        if ( type === 'other' ) {
                                // User specified time zone manually in <input>
                                // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( $tzTextbox.val() );
+                               minuteDiff = hoursToMinutes( timezoneWidget.textinput.getValue() );
                        } else {
                                // Time zone not manually specified by user
                                if ( type === 'guess' ) {
                                        // Get browser timezone & fill it in
                                        minuteDiff = -( new Date().getTimezoneOffset() );
-                                       $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                       $tzSelect.val( 'other' );
+                                       timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                       timezoneWidget.dropdowninput.setValue( 'other' );
                                } else {
-                                       // Grab data from the $tzSelect value
+                                       // Grab data from the dropdown value
                                        minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
                                }
                        }
@@ -76,9 +82,9 @@
                        $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
                }
 
-               if ( $tzSelect.length && $tzTextbox.length ) {
-                       $tzSelect.change( updateTimezoneSelection );
-                       $tzTextbox.blur( updateTimezoneSelection );
+               if ( timezoneWidget ) {
+                       timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                       timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
                        updateTimezoneSelection();
                }
 
index f7bf7a6..993f8d3 100644 (file)
@@ -105,6 +105,9 @@ $wgAutoloadClasses += [
        # tests/phpunit/includes/diff
        'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php",
 
+       # tests/phpunit/includes/externalstore
+       'ExternalStoreForTesting' => "$testDir/phpunit/includes/externalstore/ExternalStoreForTesting.php",
+
        # tests/phpunit/includes/logging
        'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php",
 
index ff574d1..82054c4 100644 (file)
@@ -15140,9 +15140,9 @@ SVG thumbnails with invalid language code
 !! options
 parsoid=wt2html,wt2wt,html2html
 !! wikitext
-[[File:Foobar.svg|thumb|caption|lang=invalid.language.code]]
+[[File:Foobar.svg|thumb|caption|lang=invalid:language:code]]
 !! html/php
-<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid.language.code</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid:language:code</div></div></div>
 
 !! html/parsoid
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" data-file-width="240" data-file-height="180" data-file-type="drawing" height="165" width="220"/></a><figcaption>lang=invalid.language.code</figcaption></figure>
@@ -18505,6 +18505,20 @@ all additional text is vanished
 <p>all additional text is vanished</p>
 !! end
 
+!! test
+Language converter glossary rules inside attributes (T119158)
+!! options
+language=sr variant=sr-el
+!! wikitext
+-{H|foAjrjvi=>sr-el:" onload="alert(1)" data-foo="}-
+
+[[File:Foobar.jpg|alt=-{}-foAjrjvi-{}-]]
+!! html
+<p>
+</p><p><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="image"><img alt="&quot; onload=&quot;alert(1)&quot; data-foo=&quot;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
 !! test
 Self closed html pairs (T7487)
 !! wikitext
index d78c1e7..b25e046 100644 (file)
@@ -150,6 +150,13 @@ class PreferencesTest extends MediaWikiTestCase {
 
        /** Helper */
        protected function prefsFor( $user_key ) {
+               // TODO This should use Preferences::getPreferences() instead of calling internal methods.
+               // Unfortunately that currently ignores the $user parameter if it has cached data, even for
+               // a different user...
+               OutputPage::setupOOUI(
+                       strtolower( $this->context->getSkin()->getSkinName() ),
+                       $this->context->getLanguage()->getDir()
+               );
                $preferences = [];
                Preferences::profilePreferences(
                        $this->prefUsers[$user_key],
index 73559f3..2fdf590 100644 (file)
@@ -159,6 +159,30 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
        }
 
+       /**
+        * @covers Revision::getRecentChange
+        */
+       public function testGetRecentChange() {
+               $rev = $this->testPage->getRevision();
+               $recentChange = $rev->getRecentChange();
+
+               // Make sure various attributes look right / the correct entry has been retrieved.
+               $this->assertEquals( $rev->getTimestamp(), $recentChange->getAttribute( 'rc_timestamp' ) );
+               $this->assertEquals(
+                       $rev->getTitle()->getNamespace(),
+                       $recentChange->getAttribute( 'rc_namespace' )
+               );
+               $this->assertEquals(
+                       $rev->getTitle()->getDBkey(),
+                       $recentChange->getAttribute( 'rc_title' )
+               );
+               $this->assertEquals( $rev->getUser(), $recentChange->getAttribute( 'rc_user' ) );
+               $this->assertEquals( $rev->getUserText(), $recentChange->getAttribute( 'rc_user_text' ) );
+               $this->assertEquals( $rev->getComment(), $recentChange->getAttribute( 'rc_comment' ) );
+               $this->assertEquals( $rev->getPage(), $recentChange->getAttribute( 'rc_cur_id' ) );
+               $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
+       }
+
        /**
         * @covers Revision::insertOn
         */
@@ -1222,4 +1246,130 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() );
        }
 
+       /**
+        * @covers Revision::newKnownCurrent
+        */
+       public function testNewKnownCurrent() {
+               // Setup the services
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $this->setService( 'MainWANObjectCache', $cache );
+               $db = wfGetDB( DB_MASTER );
+
+               // Get a fresh revision to use during testing
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev = $this->testPage->getRevision();
+
+               // Clear any previous cache for the revision during creation
+               $key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
+               $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
+               $this->assertFalse( $cache->get( $key ) );
+
+               // Get the new revision and make sure it is in the cache and correct
+               $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
+               $this->assertRevEquals( $rev, $newRev );
+               $this->assertRevEquals( $rev, $cache->get( $key ) );
+       }
+
+       public function provideUserCanBitfield() {
+               yield [ 0, 0, [], null, true ];
+               // Bitfields match, user has no permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], null, false ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], null, false ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], null, false ];
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], null, false ];
+               // Bitfields match, user (admin) does have permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], null, true ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], null, true ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], null, true ];
+               // Bitfields match, user (admin) does not have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], null, false ];
+               // Bitfields match, user (oversight) does have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], null, true ];
+               // Check permissions using the title
+               yield [
+                       Revision::DELETED_TEXT,
+                       Revision::DELETED_TEXT,
+                       [ 'sysop' ],
+                       Title::newFromText( __METHOD__ ),
+                       true,
+               ];
+               yield [
+                       Revision::DELETED_TEXT,
+                       Revision::DELETED_TEXT,
+                       [],
+                       Title::newFromText( __METHOD__ ),
+                       false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserCanBitfield
+        * @covers Revision::userCanBitfield
+        */
+       public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+               $this->setMwGlobals(
+                       'wgGroupPermissions',
+                       [
+                               'sysop' => [
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                               ],
+                               'oversight' => [
+                                       'viewsuppressed' => true,
+                                       'suppressrevision' => true,
+                               ],
+                       ]
+               );
+               $user = $this->getTestUser( $userGroups )->getUser();
+
+               $this->assertSame(
+                       $expected,
+                       Revision::userCanBitfield( $bitField, $field, $user, $title )
+               );
+       }
+
+       public function provideUserCan() {
+               yield [ 0, 0, [], true ];
+               // Bitfields match, user has no permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], false ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], false ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], false ];
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], false ];
+               // Bitfields match, user (admin) does have permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], true ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], true ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], true ];
+               // Bitfields match, user (admin) does not have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], false ];
+               // Bitfields match, user (oversight) does have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], true ];
+       }
+
+       /**
+        * @dataProvider provideUserCan
+        * @covers Revision::userCan
+        */
+       public function testUserCan( $bitField, $field, $userGroups, $expected ) {
+               $this->setMwGlobals(
+                       'wgGroupPermissions',
+                       [
+                               'sysop' => [
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                               ],
+                               'oversight' => [
+                                       'viewsuppressed' => true,
+                                       'suppressrevision' => true,
+                               ],
+                       ]
+               );
+               $user = $this->getTestUser( $userGroups )->getUser();
+               $revision = new Revision( [ 'deleted' => $bitField ] );
+
+               $this->assertSame(
+                       $expected,
+                       $revision->userCan( $field, $user )
+               );
+       }
+
 }
index 953c795..b7e410c 100644 (file)
@@ -466,4 +466,115 @@ class RevisionTest extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_returnsFalseWhenNoTextField() {
+               $this->assertFalse( Revision::getRevisionText( new stdClass() ) );
+       }
+
+       public function provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal() {
+               yield 'Just text' => [
+                       (object)[ 'old_text' => 'SomeText' ],
+                       'old_',
+                       'SomeText'
+               ];
+               // gzip string below generated with gzdeflate( 'AAAABBAAA' )
+               yield 'gzip text' => [
+                       (object)[
+                               'old_text' => "sttttr\002\022\000",
+                               'old_flags' => 'gzip'
+                       ],
+                       'old_',
+                       'AAAABBAAA'
+               ];
+               yield 'gzip text and different prefix' => [
+                       (object)[
+                               'jojo_text' => "sttttr\002\022\000",
+                               'jojo_flags' => 'gzip'
+                       ],
+                       'jojo_',
+                       'AAAABBAAA'
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_returnsDecompressedTextFieldWhenNotExternal(
+               $row,
+               $prefix,
+               $expected
+       ) {
+               $this->assertSame( $expected, Revision::getRevisionText( $row, $prefix ) );
+       }
+
+       public function provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts() {
+               yield 'Just some text' => [ 'someNonUrlText' ];
+               yield 'No second URL part' => [ 'someProtocol://' ];
+       }
+
+       /**
+        * @dataProvider provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts(
+               $text
+       ) {
+               $this->assertFalse(
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => $text,
+                                       'old_flags' => 'external',
+                               ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_noOldId() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
+               $this->assertSame(
+                       'AAAABBAAA',
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => 'ForTesting://cluster1/12345',
+                                       'old_flags' => 'external,gzip',
+                               ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_oldId() {
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $this->setService( 'MainWANObjectCache', $cache );
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
+
+               $cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
+
+               $this->assertSame(
+                       'AAAABBAAA',
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => 'ForTesting://cluster1/12345',
+                                       'old_flags' => 'external,gzip',
+                                       'old_id' => '7777',
+                               ]
+                       )
+               );
+               $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
+       }
+
 }
index 4a30292..f97dd73 100644 (file)
@@ -3,10 +3,11 @@
 /**
  * @covers Action
  *
- * @author Thiemo Mättig
- *
  * @group Action
  * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Thiemo Kreuz
  */
 class ActionTest extends MediaWikiTestCase {
 
index e0ddb0a..5f37078 100644 (file)
@@ -221,8 +221,12 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase
                $req->password = 'DoesNotExist';
                $ret = $provider->beginPrimaryAuthentication( $reqs );
                $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->beginPrimaryAuthentication( $reqs )
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
                );
 
                // Validation failure
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php b/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..a0bac63
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @covers ExternalStoreFactory
+ */
+class ExternalStoreFactoryTest extends PHPUnit_Framework_TestCase {
+
+       public function testExternalStoreFactory_noStores() {
+               $factory = new ExternalStoreFactory( [] );
+               $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) );
+               $this->assertFalse( $factory->getStoreObject( 'foo' ) );
+       }
+
+       public function provideStoreNames() {
+               yield 'Same case as construction' => [ 'ForTesting' ];
+               yield 'All lower case' => [ 'fortesting' ];
+               yield 'All upper case' => [ 'FORTESTING' ];
+               yield 'Mix of cases' => [ 'FOrTEsTInG' ];
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_protoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'ForTesting' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertInstanceOf( ExternalStoreForTesting::class, $store );
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertFalse( $store );
+       }
+
+}
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php b/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php
new file mode 100644 (file)
index 0000000..50f1e52
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+class ExternalStoreForTesting {
+
+       protected $data = [
+               'cluster1' => [
+                       '200' => 'Hello',
+                       '300' => [
+                               'Hello', 'World',
+                       ],
+                       // gzip string below generated with gzdeflate( 'AAAABBAAA' )
+                       '12345' => "sttttr\002\022\000",
+               ],
+       ];
+
+       /**
+        * Fetch data from given URL
+        * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid.
+        * @return mixed
+        */
+       public function fetchFromURL( $url ) {
+               // Based on ExternalStoreDB
+               $path = explode( '/', $url );
+               $cluster = $path[2];
+               $id = $path[3];
+               if ( isset( $path[4] ) ) {
+                       $itemID = $path[4];
+               } else {
+                       $itemID = false;
+               }
+
+               if ( !isset( $this->data[$cluster][$id] ) ) {
+                       return null;
+               }
+
+               if ( $itemID !== false
+                       && is_array( $this->data[$cluster][$id] )
+                       && isset( $this->data[$cluster][$id][$itemID] )
+               ) {
+                       return $this->data[$cluster][$id][$itemID];
+               }
+
+               return $this->data[$cluster][$id];
+       }
+
+}
index a365c4d..7ca3874 100644 (file)
@@ -1,31 +1,39 @@
 <?php
-/**
- * External Store tests
- */
 
 class ExternalStoreTest extends MediaWikiTestCase {
 
        /**
         * @covers ExternalStore::fetchFromURL
         */
-       public function testExternalFetchFromURL() {
-               $this->setMwGlobals( 'wgExternalStores', false );
+       public function testExternalFetchFromURL_noExternalStores() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [] )
+               );
 
                $this->assertFalse(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ),
                        'Deny if wgExternalStores is not set to a non-empty array'
                );
+       }
 
-               $this->setMwGlobals( 'wgExternalStores', [ 'FOO' ] );
+       /**
+        * @covers ExternalStore::fetchFromURL
+        */
+       public function testExternalFetchFromURL_someExternalStore() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
 
                $this->assertEquals(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
                        'Hello',
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ),
                        'Allow FOO://cluster1/200'
                );
                $this->assertEquals(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ),
                        'Hello',
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/300/0' ),
                        'Allow FOO://cluster1/300/0'
                );
                # Assertions for r68900
@@ -43,45 +51,3 @@ class ExternalStoreTest extends MediaWikiTestCase {
                );
        }
 }
-
-class ExternalStoreFOO {
-
-       protected $data = [
-               'cluster1' => [
-                       '200' => 'Hello',
-                       '300' => [
-                               'Hello', 'World',
-                       ],
-               ],
-       ];
-
-       /**
-        * Fetch data from given URL
-        * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid.
-        * @return mixed
-        */
-       function fetchFromURL( $url ) {
-               // Based on ExternalStoreDB
-               $path = explode( '/', $url );
-               $cluster = $path[2];
-               $id = $path[3];
-               if ( isset( $path[4] ) ) {
-                       $itemID = $path[4];
-               } else {
-                       $itemID = false;
-               }
-
-               if ( !isset( $this->data[$cluster][$id] ) ) {
-                       return null;
-               }
-
-               if ( $itemID !== false
-                       && is_array( $this->data[$cluster][$id] )
-                       && isset( $this->data[$cluster][$id][$itemID] )
-               ) {
-                       return $this->data[$cluster][$id][$itemID];
-               }
-
-               return $this->data[$cluster][$id];
-       }
-}
index 4b03fda..fe7c506 100644 (file)
@@ -6,7 +6,7 @@
  * @group JobQueue
  *
  * @licence GNU GPL v2+
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 class JobQueueMemoryTest extends PHPUnit_Framework_TestCase {
 
index 4a986b4..9fd640f 100644 (file)
@@ -5,6 +5,11 @@
  */
 class SvgTest extends MediaWikiMediaTestCase {
 
+       /**
+        * @var SvgHandler
+        */
+       private $handler;
+
        protected function setUp() {
                parent::setUp();
 
@@ -38,4 +43,71 @@ class SvgTest extends MediaWikiMediaTestCase {
                        [ 'Wikimedia-logo.svg', [] ]
                ];
        }
+
+       /**
+        * @param string $userPreferredLanguage
+        * @param array $svgLanguages
+        * @param string $expectedMatch
+        * @dataProvider providerGetMatchedLanguage
+        * @covers SvgHandler::getMatchedLanguage
+        */
+       public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) {
+               $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages );
+               $this->assertEquals( $expectedMatch, $match );
+       }
+
+       public function providerGetMatchedLanguage() {
+               return [
+                       'no match' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ],
+                               'expectedMatch' => null,
+                       ],
+                       'no subtags' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ],
+                               'expectedMatch' => 'en',
+                       ],
+                       'user no subtags, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-GB',
+                       ],
+                       'user no subtags, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Cyrl-BA',
+                       ],
+                       'user 1 subtag, svg no subtags' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de', 'en', 'en', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'user 1 subtag, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-US',
+                       ],
+                       'user 1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg <=1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'ensure case-insensitive' => [
+                               'userPreferredLanguage' => 'sr-latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+               ];
+       }
 }
index 930bbe4..b1d8c69 100644 (file)
@@ -9,7 +9,7 @@
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  * @author Daniel Kinzler
  * @author Addshore
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 abstract class SpecialPageTestBase extends MediaWikiTestCase {
 
index 0da03df..950d2df 100644 (file)
@@ -6,7 +6,7 @@
  * @group Language
  *
  * @license GPL-2.0+
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 class LanguageCodeTest extends PHPUnit_Framework_TestCase {
 
index 81184aa..fc2ed33 100644 (file)
@@ -157,6 +157,25 @@ class LanguageConverterTest extends MediaWikiLangTestCase {
                $wgRequest->setVal( 'variant', null );
                $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
        }
+
+       /**
+        * Test exhausting pcre.backtrack_limit
+        */
+       public function testAutoConvertT124404() {
+               $testString = '';
+               for ( $i = 0; $i < 1000; $i++ ) {
+                       $testString .= 'xxx xxx xxx';
+               }
+               $testString .= "\n<big id='в'></big>";
+               $old = ini_set( 'pcre.backtrack_limit', 200 );
+               $result = $this->lc->autoConvert( $testString, 'tg-latn' );
+               ini_set( 'pcre.backtrack_limit', $old );
+               // The в in the id attribute should not get converted to a v
+               $this->assertFalse(
+                       strpos( $result, 'v' ),
+                       "в converted to v despite being in attribue"
+               );
+       }
 }
 
 /**
index 98b87fe..890fe5b 100644 (file)
@@ -3,8 +3,8 @@ const Page = require( './page' );
 
 class PreferencesPage extends Page {
 
-       get realName() { return browser.element( '#mw-input-wprealname' ); }
-       get save() { return browser.element( '#prefcontrol' ); }
+       get realName() { return browser.element( '#mw-input-wprealname .oo-ui-inputWidget-input' ); }
+       get save() { return browser.element( '#prefcontrol .oo-ui-buttonElement-button' ); }
 
        open() {
                super.open( 'Special:Preferences' );