Merge "Add actual documentation for ContentHandler::getActionOverrides"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 18 May 2016 17:41:05 +0000 (17:41 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 18 May 2016 17:41:05 +0000 (17:41 +0000)
55 files changed:
autoload.php
includes/MediaWikiServices.php
includes/ServiceWiring.php
includes/Title.php
includes/WatchedItemStore.php
includes/actions/Action.php
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiStashEdit.php
includes/api/i18n/de.json
includes/api/i18n/he.json
includes/api/i18n/ko.json
includes/debug/MWDebug.php
includes/diff/DairikiDiff.php
includes/diff/DiffEngine.php [new file with mode: 0644]
includes/diff/WikiDiff3.php [deleted file]
includes/diff/WordAccumulator.php [new file with mode: 0644]
includes/diff/WordLevelDiff.php [new file with mode: 0644]
includes/interwiki/ClassicInterwikiLookup.php [new file with mode: 0644]
includes/interwiki/Interwiki.php
includes/interwiki/InterwikiLookup.php [new file with mode: 0644]
includes/skins/SkinTemplate.php
languages/i18n/azb.json
languages/i18n/ba.json
languages/i18n/be-tarask.json
languages/i18n/bgn.json
languages/i18n/cs.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/eo.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/it.json
languages/i18n/jv.json
languages/i18n/ko.json
languages/i18n/nds-nl.json
languages/i18n/nds.json
languages/i18n/pl.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/ses.json
languages/i18n/zh-hant.json
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/LinkerTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/TestUser.php
tests/phpunit/includes/WatchedItemStoreIntegrationTest.php
tests/phpunit/includes/WatchedItemStoreUnitTest.php
tests/phpunit/includes/actions/ActionTest.php
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/debug/MWDebugTest.php
tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php [new file with mode: 0644]
tests/phpunit/includes/interwiki/InterwikiTest.php
tests/phpunit/includes/session/BotPasswordSessionProviderTest.php
tests/phpunit/includes/user/BotPasswordTest.php

index cf0e417..aeb69fd 100644 (file)
@@ -352,7 +352,7 @@ $wgAutoloadLocalClasses = [
        'DerivativeResourceLoaderContext' => __DIR__ . '/includes/resourceloader/DerivativeResourceLoaderContext.php',
        'DescribeFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
        'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php',
-       'DiffEngine' => __DIR__ . '/includes/diff/DairikiDiff.php',
+       'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php',
        'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php',
        'DiffHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
        'DiffOp' => __DIR__ . '/includes/diff/DairikiDiff.php',
@@ -779,7 +779,6 @@ $wgAutoloadLocalClasses = [
        'MalformedTitleException' => __DIR__ . '/includes/title/MalformedTitleException.php',
        'ManualLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
        'MapCacheLRU' => __DIR__ . '/includes/libs/MapCacheLRU.php',
-       'MappedDiff' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php',
        'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php',
        'McTest' => __DIR__ . '/maintenance/mctest.php',
@@ -794,6 +793,8 @@ $wgAutoloadLocalClasses = [
        'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
        'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
        'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
+       'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php',
+       'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php',
        'MediaWiki\\Auth\\AbstractAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractAuthenticationProvider.php',
        'MediaWiki\\Auth\\AbstractPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php',
        'MediaWiki\\Auth\\AbstractPreAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPreAuthenticationProvider.php',
@@ -829,6 +830,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Auth\\Throttler' => __DIR__ . '/includes/auth/Throttler.php',
        'MediaWiki\\Auth\\UserDataAuthenticationRequest' => __DIR__ . '/includes/auth/UserDataAuthenticationRequest.php',
        'MediaWiki\\Auth\\UsernameAuthenticationRequest' => __DIR__ . '/includes/auth/UsernameAuthenticationRequest.php',
+       'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
        'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
@@ -1087,7 +1089,7 @@ $wgAutoloadLocalClasses = [
        'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php',
        'RSSFeed' => __DIR__ . '/includes/Feed.php',
        'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php',
-       'RangeDifference' => __DIR__ . '/includes/diff/WikiDiff3.php',
+       'RangeDifference' => __DIR__ . '/includes/diff/DiffEngine.php',
        'RawAction' => __DIR__ . '/includes/actions/RawAction.php',
        'RawMessage' => __DIR__ . '/includes/Message.php',
        'ReadOnlyError' => __DIR__ . '/includes/exception/ReadOnlyError.php',
@@ -1505,7 +1507,6 @@ $wgAutoloadLocalClasses = [
        'WebRequestUpload' => __DIR__ . '/includes/WebRequestUpload.php',
        'WebResponse' => __DIR__ . '/includes/WebResponse.php',
        'WikiCategoryPage' => __DIR__ . '/includes/page/WikiCategoryPage.php',
-       'WikiDiff3' => __DIR__ . '/includes/diff/WikiDiff3.php',
        'WikiExporter' => __DIR__ . '/includes/export/WikiExporter.php',
        'WikiFilePage' => __DIR__ . '/includes/page/WikiFilePage.php',
        'WikiImporter' => __DIR__ . '/includes/import/WikiImporter.php',
@@ -1518,7 +1519,7 @@ $wgAutoloadLocalClasses = [
        'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php',
        'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php',
        'WithoutInterwikiPage' => __DIR__ . '/includes/specials/SpecialWithoutinterwiki.php',
-       'WordLevelDiff' => __DIR__ . '/includes/diff/DairikiDiff.php',
+       'WordLevelDiff' => __DIR__ . '/includes/diff/WordLevelDiff.php',
        'WrapOldPasswords' => __DIR__ . '/maintenance/wrapOldPasswords.php',
        'XCFHandler' => __DIR__ . '/includes/media/XCF.php',
        'XCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/XCacheBagOStuff.php',
index 71e58af..e2dc691 100644 (file)
@@ -24,6 +24,7 @@ use WatchedItemStore;
 use SkinFactory;
 use TitleFormatter;
 use TitleParser;
+use MediaWiki\Interwiki\InterwikiLookup;
 
 /**
  * Service locator for MediaWiki core services.
@@ -384,6 +385,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'SiteStore' );
        }
 
+       /**
+        * @since 1.28
+        * @return InterwikiLookup
+        */
+       public function getInterwikiLookup() {
+               return $this->getService( 'InterwikiLookup' );
+       }
+
        /**
         * @since 1.27
         * @return StatsdDataFactory
index 293e6eb..6bdacf0 100644 (file)
@@ -37,6 +37,7 @@
  *      MediaWiki code base.
  */
 
+use MediaWiki\Interwiki\ClassicInterwikiLookup;
 use MediaWiki\MediaWikiServices;
 
 return [
@@ -88,6 +89,19 @@ return [
                return $services->getConfigFactory()->makeConfig( 'main' );
        },
 
+       'InterwikiLookup' => function( MediaWikiServices $services ) {
+               global $wgContLang; // TODO: manage $wgContLang as a service
+               $config = $services->getMainConfig();
+               return new ClassicInterwikiLookup(
+                       $wgContLang,
+                       ObjectCache::getMainWANInstance(),
+                       $config->get( 'InterwikiExpiry' ),
+                       $config->get( 'InterwikiCache' ),
+                       $config->get( 'InterwikiScopes' ),
+                       $config->get( 'InterwikiFallbackSite' )
+               );
+       },
+
        'StatsdDataFactory' => function( MediaWikiServices $services ) {
                return new BufferingStatsdDataFactory(
                        rtrim( $services->getMainConfig()->get( 'StatsdMetricPrefix' ), '.' )
index 25fbce3..72c21fc 100644 (file)
@@ -908,7 +908,9 @@ class Title implements LinkTarget {
         * @return string Content model id
         */
        public function getContentModel( $flags = 0 ) {
-               if ( !$this->mContentModel && $this->getArticleID( $flags ) ) {
+               if ( ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE ) &&
+                       $this->getArticleID( $flags )
+               ) {
                        $linkCache = LinkCache::singleton();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
                        $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
index eb652ce..f0619d6 100644 (file)
@@ -145,16 +145,28 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        private function uncacheLinkTarget( LinkTarget $target ) {
+               $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
                if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
                        return;
                }
-               $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
                foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
                        $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
                        $this->cache->delete( $key );
                }
        }
 
+       private function uncacheUser( User $user ) {
+               $this->stats->increment( 'WatchedItemStore.uncacheUser' );
+               foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
+                       foreach ( $dbKeyArray as $dbKey => $userArray ) {
+                               if ( isset( $userArray[$user->getId()] ) ) {
+                                       $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
+                                       $this->cache->delete( $userArray[$user->getId()] );
+                               }
+                       }
+               }
+       }
+
        /**
         * @param User $user
         * @param LinkTarget $target
@@ -667,6 +679,41 @@ class WatchedItemStore implements StatsdAwareInterface {
                return $success;
        }
 
+       /**
+        * @param User $user The user to set the timestamp for
+        * @param string $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 = [] ) {
+               // Only loggedin user can have a watchlist
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               $dbw = $this->getConnection( DB_MASTER );
+
+               $conds = [ 'wl_user' => $user->getId() ];
+               if ( $targets ) {
+                       $batch = new LinkBatch( $targets );
+                       $conds[] = $batch->constructSet( 'wl', $dbw );
+               }
+
+               $success = $dbw->update(
+                       'watchlist',
+                       [ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ],
+                       $conds,
+                       __METHOD__
+               );
+
+               $this->reuseConnection( $dbw );
+
+               $this->uncacheUser( $user );
+
+               return $success;
+       }
+
        /**
         * @param User $editor The editor that triggered the update. Their notification
         *  timestamp will not be updated(they have already seen it)
index 84bf16e..ae4c6d6 100644 (file)
@@ -147,7 +147,7 @@ abstract class Action {
                // Trying to get a WikiPage for NS_SPECIAL etc. will result
                // in WikiPage::factory throwing "Invalid or virtual namespace -1 given."
                // For SpecialPages et al, default to action=view.
-               if ( !$context->canUseWikiPage() ) {
+               if ( $actionName === '' || !$context->canUseWikiPage() ) {
                        return 'view';
                }
 
index ea52e14..a299e87 100644 (file)
@@ -24,6 +24,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * API interface for setting the wl_notificationtimestamp field
@@ -98,13 +99,14 @@ class ApiSetNotificationTimestamp extends ApiBase {
                        }
                }
 
+               $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
                $apiResult = $this->getResult();
                $result = [];
                if ( $params['entirewatchlist'] ) {
                        // Entire watchlist mode: Just update the thing and return a success indicator
-                       $dbw->update( 'watchlist', [ 'wl_notificationtimestamp' => $timestamp ],
-                               [ 'wl_user' => $user->getId() ],
-                               __METHOD__
+                       $watchedItemStore->setNotificationTimestampsForUser(
+                               $user,
+                               $timestamp
                        );
 
                        $result['notificationtimestamp'] = is_null( $timestamp )
@@ -133,14 +135,15 @@ class ApiSetNotificationTimestamp extends ApiBase {
 
                        if ( $pageSet->getTitles() ) {
                                // Now process the valid titles
-                               $lb = new LinkBatch( $pageSet->getTitles() );
-                               $dbw->update( 'watchlist', [ 'wl_notificationtimestamp' => $timestamp ],
-                                       [ 'wl_user' => $user->getId(), $lb->constructSet( 'wl', $dbw ) ],
-                                       __METHOD__
+                               $watchedItemStore->setNotificationTimestampsForUser(
+                                       $user,
+                                       $timestamp,
+                                       $pageSet->getTitles()
                                );
 
                                // Query the results of our update
                                $timestamps = [];
+                               $lb = new LinkBatch( $pageSet->getTitles() );
                                $res = $dbw->select(
                                        'watchlist',
                                        [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
index 3539eed..93003cc 100644 (file)
@@ -294,9 +294,17 @@ class ApiStashEdit extends ApiBase {
                        $logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." );
                        return $editInfo; // assume nothing changed
                } elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) {
+                       // Logged-in user made no local upload/template edits in the meantime
                        $stats->increment( 'editstash.cache_hits.presumed_fresh' );
                        $logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." );
-                       return $editInfo; // use made no local upload/template edits in the meantime
+                       return $editInfo;
+               } elseif ( $user->isAnon()
+                       && self::lastEditTime( $user ) < $editInfo->output->getCacheTime()
+               ) {
+                       // Logged-out user made no local upload/template edits in the meantime
+                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                       $logger->debug( "Edit check based cache hit for key '$key' (age: $age sec)." );
+                       return $editInfo;
                }
 
                $dbr = wfGetDB( DB_SLAVE );
@@ -359,6 +367,21 @@ class ApiStashEdit extends ApiBase {
                return $editInfo;
        }
 
+       /**
+        * @param User $user
+        * @return string|null TS_MW timestamp or null
+        */
+       private static function lastEditTime( User $user ) {
+               $time = wfGetDB( DB_SLAVE )->selectField(
+                       'recentchanges',
+                       'MAX(rc_timestamp)',
+                       [ 'rc_user_text' => $user->getName() ],
+                       __METHOD__
+               );
+
+               return wfTimestampOrNull( TS_MW, $time );
+       }
+
        /**
         * Get the temporary prepared edit stash key for a user
         *
@@ -371,7 +394,7 @@ class ApiStashEdit extends ApiBase {
         * @param User $user User to get parser options from
         * @return string
         */
-       protected static function getStashKey( Title $title, Content $content, User $user ) {
+       private static function getStashKey( Title $title, Content $content, User $user ) {
                $hash = sha1( implode( ':', [
                        $content->getModel(),
                        $content->getDefaultFormat(),
@@ -394,7 +417,7 @@ class ApiStashEdit extends ApiBase {
         * @param User $user
         * @return array (stash info array, TTL in seconds) or (null, 0)
         */
-       protected static function buildStashValue(
+       private static function buildStashValue(
                Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user
        ) {
                // If an item is renewed, mind the cache TTL determined by config and parser functions.
index 5402ab4..938a61a 100644 (file)
        "apihelp-import-param-namespace": "In diesen Namensraum importieren. Kann nicht zusammen mit <var>$1rootpage</var> verwendet werden.",
        "apihelp-import-param-rootpage": "Als Unterseite dieser Seite importieren. Kann nicht zusammen mit <var>$1namespace</var> verwendet werden.",
        "apihelp-import-example-import": "Importiere [[meta:Help:ParserFunctions]] mit der kompletten Versionsgeschichte in den Namensraum 100.",
-       "apihelp-login-description": "Anmelden und Authentifizierungs-Cookies beziehen.\n\nFalls das Anmelden erfolgreich war, werden die benötigten Cookies im Header der HTTP-Antwort des Servers übermittelt. Bei fehlgeschlagenen Anmeldeversuchen können weitere Versuche gedrosselt werden, um automatische Passwortermittlungsattacken zu verhinden.",
+       "apihelp-login-description": "Anmelden und Authentifizierungs-Cookies beziehen.\n\nDiese Aktion sollte nur in Kombination mit [[Special:BotPasswords]] verwendet werden. Die Verwendung für die Anmeldung beim Hauptkonto ist veraltet und kann ohne Warnung fehlschlagen. Um sich sicher beim Hauptkonto anzumelden, verwende <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
        "apihelp-login-param-name": "Benutzername.",
        "apihelp-login-param-password": "Passwort.",
        "apihelp-login-param-domain": "Domain (optional).",
        "apihelp-query+usercontribs-paramvalue-prop-ids": "Fügt die Seiten- und Versionskennung hinzu.",
        "apihelp-query+usercontribs-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel der Bearbeitung.",
        "apihelp-query+usercontribs-paramvalue-prop-comment": "Fügt den Kommentar der Bearbeitung hinzu.",
+       "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Markiert kontrollierte Bearbeitungen.",
+       "apihelp-query+usercontribs-paramvalue-prop-tags": "Listet die Markierungen für die Bearbeitung auf.",
        "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Markiert, ob der aktuelle Benutzer gesperrt ist, von wem und aus welchem Grund.",
        "apihelp-query+userinfo-paramvalue-prop-editcount": "Ergänzt den Bearbeitungszähler des aktuellen Benutzers.",
        "apihelp-query+userinfo-paramvalue-prop-realname": "Fügt den bürgerlichen Namen des Benutzers hinzu.",
        "apihelp-query+users-paramvalue-prop-implicitgroups": "Listet alle Gruppen auf, bei denen der Benutzer automatisch Mitglied ist.",
        "apihelp-query+users-paramvalue-prop-rights": "Listet alle Rechte auf, die jeder Benutzer hat.",
        "apihelp-query+users-paramvalue-prop-editcount": "Ergänzt den Bearbeitungszähler des Benutzers.",
+       "apihelp-query+users-param-users": "Eine Liste der Benutzer, für die Informationen abgerufen werden sollen.",
        "apihelp-query+users-example-simple": "Gibt Informationen für den Benutzer <kbd>Example</kbd> zurück.",
+       "apihelp-query+watchlist-param-user": "Listet nur Änderungen von diesem Benutzer auf.",
+       "apihelp-query+watchlist-param-excludeuser": "Listet keine Änderungen von diesem Benutzer auf.",
        "apihelp-query+watchlist-param-prop": "Zusätzlich zurückzugebende Eigenschaften:",
+       "apihelp-query+watchlist-paramvalue-prop-ids": "Ergänzt die Versions- und Seitenkennungen.",
+       "apihelp-query+watchlist-paramvalue-prop-title": "Ergänzt den Titel der Seite.",
+       "apihelp-query+watchlist-paramvalue-prop-flags": "Ergänzt die Markierungen für die Bearbeitungen.",
        "apihelp-query+watchlist-paramvalue-prop-user": "Ergänzt den Benutzer, der die Bearbeitung ausgeführt hat.",
        "apihelp-query+watchlist-paramvalue-prop-userid": "Ergänzt die Kennung des Benutzers, der die Bearbeitung ausgeführt hat.",
        "apihelp-query+watchlist-paramvalue-prop-comment": "Ergänzt den Kommentar der Bearbeitung.",
index 7a842f4..92d14bd 100644 (file)
@@ -40,6 +40,8 @@
        "apihelp-block-param-watchuser": "לעקוב אחרי דף המשתמש ודף השיחה של המשתמש או של כתובת ה־IP.",
        "apihelp-block-example-ip-simple": "חסימת כתובת ה־IP‏ <kbd>192.0.2.5</kbd> לשלושה ימים עם הסיבה <kbd>First strike</kbd>.",
        "apihelp-block-example-user-complex": "חסימת המשתמש <kbd>Vandal</kbd> ללא הגבלת זמן עם הסיבה <kbd>Vandalism</kbd>, ומניעת יצירת חשבונות חדשים ושליחת דוא\"ל.",
+       "apihelp-changeauthenticationdata-description": "שינוי נתוני אימות עבור המשתמש הנוכחי.",
+       "apihelp-changeauthenticationdata-example-password": "ניסיון לשנות את הססמה של המשתמש הנוכחי ל־<kbd>ExamplePassword</kbd>.",
        "apihelp-checktoken-description": "בדיקת התקינות של האסימון מ־<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
        "apihelp-checktoken-param-type": "סוג האסימון שבבדיקה.",
        "apihelp-checktoken-param-token": "איזה אסימון לבדוק.",
@@ -47,6 +49,9 @@
        "apihelp-checktoken-example-simple": "בדיקת התקינות של אסימון <kbd>csrf</kbd>.",
        "apihelp-clearhasmsg-description": "מנקה את דגל <code>hasmsg</code> עבור המשתמש הנוכחי.",
        "apihelp-clearhasmsg-example-1": "לנקות את דגל <code>hasmsg</code> עבור המשתמש הנוכחי.",
+       "apihelp-clientlogin-description": "כניסה לוויקי באמצעות זרימה הידודית.",
+       "apihelp-clientlogin-example-login": "תחילת תהליך כניסה לוויקי בתור משתמש <kbd>Example</kbd> עם הססמה <kbd>ExamplePassword</kbd>.",
+       "apihelp-clientlogin-example-login2": "המשך כניסה אחרי תשובת UI לאימות דו־גורמי, עם <var>OATHToken</var> של <kbd>987654</kbd>.",
        "apihelp-compare-description": "קבלת ההבדל בין 2 דפים.\n\nיש להעביר מספר גרסה, כותרת דף או מזהה דף גם ל־\"from\" וגם ל־\"to\".",
        "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.",
        "apihelp-compare-param-fromid": "מס׳ זיהוי של העמוד הראשון להשוואה.",
@@ -56,6 +61,7 @@
        "apihelp-compare-param-torev": "גרסה שנייה להשוואה.",
        "apihelp-compare-example-1": "יצירת תיעוד שינוי בין גרסה 1 ל־2.",
        "apihelp-createaccount-description": "יצירת חשבון משתמש חדש.",
+       "apihelp-createaccount-example-create": "תחילת תהליך יצירת המשתמש <kbd>Example</kbd> עם הססמה <kbd>ExamplePassword</kbd>.",
        "apihelp-createaccount-param-name": "שם משתמש.",
        "apihelp-createaccount-param-password": "ססמה (לא ישפיע אם הוגדר <var>$1mailpassword</var>).",
        "apihelp-createaccount-param-domain": "שם מתחם לאימות חיצוני (רשות).",
        "apihelp-import-param-namespace": "לייבא למרחב השם הזה. לא ניתן להשתמש בזה יחד עם <var>$1rootpage</var>.",
        "apihelp-import-param-rootpage": "לייבא בתור תת־משנה של הדף הזה. לא ניתן להשתמש בזה יחד עם <var>$1namespace</var>.",
        "apihelp-import-example-import": "לייבא את [[meta:Help:ParserFunctions]] למרחב השם 100 עם היסטוריה מלאה.",
-       "apihelp-login-description": "להיכנס ולקבל עוגיות אימות.\n\nבמקרה של כניסה מוצלחת, העוגיות הדרושות תיכללנה בכותרות תשובות של HTTP. במקרה של כניסה לא מוצלחת, הניסיונות הבאים עשויים להיות חנוקים כדי להגביל תקיפות ניחוש ססמה אוטומטי.",
+       "apihelp-linkaccount-description": "קישור חשבון של ספק צד־שלישי למשתמש הנוכחי.",
+       "apihelp-linkaccount-example-link": "תחילת תהליך הקישור לחשבון מ־<kbd>Example</kbd>.",
+       "apihelp-login-description": "להיכנס ולקבל עוגיות אימות.\n\nהפעולה הזאת צריכה לשמש רק בשילוב [[Special:BotPasswords]]; שימוש לכניסה לחשבון ראשי מיושן ועשוי להיכשל ללא אזהרה. כדי להיכנס בבטחה לחשבון הראשי, יש להשתמש ב־<kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nobotpasswords": "להיכנס ולקבל עוגיות אימות.\n\nהפעולה הזאת מיושנת ועשויה להיכשל ללא אזהרה. כדי להיכנס בבטחה, יש להשתמש ב־<kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nonauthmanager": "להיכנס ולקבל עוגיות אימות.\n\nבמקרה של כניסה מוצלחת, העוגיות המקוננות תיכללנה בכותרות תשובות ה־HTTP. במקרה של כניסה כושלת, הניסיונות הבאים יוגבלו למספר ניסויי ניחוש הססמה האוטומטיים.",
        "apihelp-login-param-name": "שם משתמש.",
        "apihelp-login-param-password": "ססמה.",
        "apihelp-login-param-domain": "שם מתחם (רשות).",
        "apihelp-query+allusers-param-activeusers": "לרשום רק משתמשים שהיו פעילים {{PLURAL:$1|ביום האחרון|ביומיים האחרונים|ב־$1 הימים האחרונים}}.",
        "apihelp-query+allusers-param-attachedwiki": "עם <kbd>$1prop=centralids</kbd>, לציין גם האם המשתמש משויך לוויקי עם המזהה הזה.",
        "apihelp-query+allusers-example-Y": "לרשום משתמשים שמתחילים ב־<kbd>Y</kbd>.",
+       "apihelp-query+authmanagerinfo-description": "אחזור מידע אודות מצב האימות הנוכחי.",
        "apihelp-query+backlinks-description": "מציאת כל הדפים שמקשרים לדף הנתון.",
        "apihelp-query+backlinks-param-title": "איזו כותרת לחפש. לא ניתן להשתמש בזה יחד עם <var>$1pageid</var>.",
        "apihelp-query+backlinks-param-pageid": "מזהה דף לחיפוש. לא ניתן להשתמש בזה יחד עם <var>$1title</var>.",
        "apihelp-query+watchlistraw-param-totitle": "באיזו כותרת (עם תחילית מרחב שם) להפסיק למנות.",
        "apihelp-query+watchlistraw-example-simple": "לרשום דפים ברשימת המעקב של המשתמש הנוכחי.",
        "apihelp-query+watchlistraw-example-generator": "אחזור מידע על הדפים עבור דפים ברשימת המעקב של המשתמש הנוכחי.",
+       "apihelp-removeauthenticationdata-description": "הסרת נתוני אימות עבור המשתמש הנוכחי.",
+       "apihelp-resetpassword-description": "שליחת דוא\"ל איפוס סיסמה למשתמש.",
+       "apihelp-resetpassword-param-email": "כתובת הדוא\"ל של המשתמש שהסיסמה שלו מאופסת.",
        "apihelp-revisiondelete-description": "מחיקה ושחזור ממחיקה של גרסאות.",
        "apihelp-revisiondelete-param-type": "סוג מחיקת הגרסה שמתבצע.",
        "apihelp-revisiondelete-param-target": "שם הדף למחיקת גרסה, אם זה נחוץ לסוג.",
        "apihelp-undelete-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.",
        "apihelp-undelete-example-page": "שחזור ממחיקה של הדף <kbd>Main Page</kbd>.",
        "apihelp-undelete-example-revisions": "שחזור שתי גרסאות של הדף <kbd>Main Page</kbd>.",
+       "apihelp-unlinkaccount-description": "ביטול קישור של חשבון צד־שלישי מהמשתמש הנוכחי.",
        "apihelp-upload-description": "העלאת קובץ, או קבלת מצב ההעלאות הממתינות.\n\nיש מספר שיטות:\n* להעלות את הקובץ ישירות, באמצעות הפרמטר <var>$1file</var>.\n* להעלות את הקובץ בחלקים, באמצעות הפרמטרים <var>$1filesize</var>‏, <var>$1chunk</var> ו־<var>$1offset</var>.\n* לגרום לשרת מדיה־ויקי לאחזר את הקובץ מ־URL באמצעות הפרמטר <var>$1url</var>.\n* להשלים העלאה קודמת שנכשלה בשל אזהרות באמצעות הפרמטר <var>$1filekey</var>.\nלתשומך לבך, יש לעשות את HTTP POST בתור העלאת קובץ (כלומר באמצעות <code>multipart/form-data</code>) בעת שליחת ה־<var>$1file</var>.",
        "apihelp-upload-param-filename": "שם קובץ היעד.",
        "apihelp-upload-param-comment": "הערת העלאה. משמש גם בתור טקסט הדף ההתחלתי עבור קבצים חדשים אם <var>$1text</var> אינו מצוין.",
index a33dedf..ccfc005 100644 (file)
@@ -49,9 +49,9 @@
        "apihelp-compare-param-fromtitle": "비교할 첫 이름.",
        "apihelp-compare-param-fromid": "비교할 첫 문서 ID.",
        "apihelp-compare-param-fromrev": "비교할 첫 판.",
-       "apihelp-compare-param-totitle": "비교할 두번째 제목.",
-       "apihelp-compare-param-toid": "비교할 두번째 문서 ID.",
-       "apihelp-compare-param-torev": "비교할 두번째 판.",
+       "apihelp-compare-param-totitle": "비교할 두 번째 제목.",
+       "apihelp-compare-param-toid": "비교할 두 번째 문서 ID.",
+       "apihelp-compare-param-torev": "비교할 두 번째 판.",
        "apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.",
        "apihelp-createaccount-description": "새 사용자 계정을 만듭니다.",
        "apihelp-createaccount-param-name": "사용자 이름",
index 13d25a8..d90ef8a 100644 (file)
@@ -75,6 +75,15 @@ class MWDebug {
                self::$enabled = true;
        }
 
+       /**
+        * Disable the debugger.
+        *
+        * @since 1.28
+        */
+       public static function deinit() {
+               self::$enabled = false;
+       }
+
        /**
         * Add ResourceLoader modules to the OutputPage object if debugging is
         * enabled.
index e5e082f..dbb32e6 100644 (file)
@@ -191,244 +191,6 @@ class DiffOpChange extends DiffOp {
        }
 }
 
-/**
- * Class used internally by Diff to actually compute the diffs.
- *
- * The algorithm used here is mostly lifted from the perl module
- * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
- *     http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
- *
- * More ideas are taken from:
- *     http://www.ics.uci.edu/~eppstein/161/960229.html
- *
- * Some ideas (and a bit of code) are from analyze.c, from GNU
- * diffutils-2.7, which can be found at:
- *     ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
- *
- * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
- * are my own.
- *
- * Line length limits for robustness added by Tim Starling, 2005-08-31
- * Alternative implementation added by Guy Van den Broeck, 2008-07-30
- *
- * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
- * @private
- * @ingroup DifferenceEngine
- */
-class DiffEngine {
-       const MAX_XREF_LENGTH = 10000;
-
-       protected $xchanged, $ychanged;
-
-       protected $xv = [], $yv = [];
-       protected $xind = [], $yind = [];
-
-       protected $seq = [], $in_seq = [];
-
-       protected $lcs = 0;
-
-       /**
-        * @param string[] $from_lines
-        * @param string[] $to_lines
-        *
-        * @return DiffOp[]
-        */
-       public function diff( $from_lines, $to_lines ) {
-
-               // Diff and store locally
-               $this->diffLocal( $from_lines, $to_lines );
-
-               // Merge edits when possible
-               $this->shiftBoundaries( $from_lines, $this->xchanged, $this->ychanged );
-               $this->shiftBoundaries( $to_lines, $this->ychanged, $this->xchanged );
-
-               // Compute the edit operations.
-               $n_from = count( $from_lines );
-               $n_to = count( $to_lines );
-
-               $edits = [];
-               $xi = $yi = 0;
-               while ( $xi < $n_from || $yi < $n_to ) {
-                       assert( $yi < $n_to || $this->xchanged[$xi] );
-                       assert( $xi < $n_from || $this->ychanged[$yi] );
-
-                       // Skip matching "snake".
-                       $copy = [];
-                       while ( $xi < $n_from && $yi < $n_to
-                               && !$this->xchanged[$xi] && !$this->ychanged[$yi]
-                       ) {
-                               $copy[] = $from_lines[$xi++];
-                               ++$yi;
-                       }
-                       if ( $copy ) {
-                               $edits[] = new DiffOpCopy( $copy );
-                       }
-
-                       // Find deletes & adds.
-                       $delete = [];
-                       while ( $xi < $n_from && $this->xchanged[$xi] ) {
-                               $delete[] = $from_lines[$xi++];
-                       }
-
-                       $add = [];
-                       while ( $yi < $n_to && $this->ychanged[$yi] ) {
-                               $add[] = $to_lines[$yi++];
-                       }
-
-                       if ( $delete && $add ) {
-                               $edits[] = new DiffOpChange( $delete, $add );
-                       } elseif ( $delete ) {
-                               $edits[] = new DiffOpDelete( $delete );
-                       } elseif ( $add ) {
-                               $edits[] = new DiffOpAdd( $add );
-                       }
-               }
-
-               return $edits;
-       }
-
-       /**
-        * @param string[] $from_lines
-        * @param string[] $to_lines
-        */
-       private function diffLocal( $from_lines, $to_lines ) {
-               $wikidiff3 = new WikiDiff3();
-               $wikidiff3->diff( $from_lines, $to_lines );
-               $this->xchanged = $wikidiff3->removed;
-               $this->ychanged = $wikidiff3->added;
-       }
-
-       /**
-        * Adjust inserts/deletes of identical lines to join changes
-        * as much as possible.
-        *
-        * We do something when a run of changed lines include a
-        * line at one end and has an excluded, identical line at the other.
-        * We are free to choose which identical line is included.
-        * `compareseq' usually chooses the one at the beginning,
-        * but usually it is cleaner to consider the following identical line
-        * to be the "change".
-        *
-        * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
-        */
-       private function shiftBoundaries( $lines, &$changed, $other_changed ) {
-               $i = 0;
-               $j = 0;
-
-               assert( count( $lines ) == count( $changed ) );
-               $len = count( $lines );
-               $other_len = count( $other_changed );
-
-               while ( 1 ) {
-                       /*
-                        * Scan forwards to find beginning of another run of changes.
-                        * Also keep track of the corresponding point in the other file.
-                        *
-                        * Throughout this code, $i and $j are adjusted together so that
-                        * the first $i elements of $changed and the first $j elements
-                        * of $other_changed both contain the same number of zeros
-                        * (unchanged lines).
-                        * Furthermore, $j is always kept so that $j == $other_len or
-                        * $other_changed[$j] == false.
-                        */
-                       while ( $j < $other_len && $other_changed[$j] ) {
-                               $j++;
-                       }
-
-                       while ( $i < $len && !$changed[$i] ) {
-                               assert( $j < $other_len && ! $other_changed[$j] );
-                               $i++;
-                               $j++;
-                               while ( $j < $other_len && $other_changed[$j] ) {
-                                       $j++;
-                               }
-                       }
-
-                       if ( $i == $len ) {
-                               break;
-                       }
-
-                       $start = $i;
-
-                       // Find the end of this run of changes.
-                       while ( ++$i < $len && $changed[$i] ) {
-                               continue;
-                       }
-
-                       do {
-                               /*
-                                * Record the length of this run of changes, so that
-                                * we can later determine whether the run has grown.
-                                */
-                               $runlength = $i - $start;
-
-                               /*
-                                * Move the changed region back, so long as the
-                                * previous unchanged line matches the last changed one.
-                                * This merges with previous changed regions.
-                                */
-                               while ( $start > 0 && $lines[$start - 1] == $lines[$i - 1] ) {
-                                       $changed[--$start] = 1;
-                                       $changed[--$i] = false;
-                                       while ( $start > 0 && $changed[$start - 1] ) {
-                                               $start--;
-                                       }
-                                       assert( $j > 0 );
-                                       while ( $other_changed[--$j] ) {
-                                               continue;
-                                       }
-                                       assert( $j >= 0 && !$other_changed[$j] );
-                               }
-
-                               /*
-                                * Set CORRESPONDING to the end of the changed run, at the last
-                                * point where it corresponds to a changed run in the other file.
-                                * CORRESPONDING == LEN means no such point has been found.
-                                */
-                               $corresponding = $j < $other_len ? $i : $len;
-
-                               /*
-                                * Move the changed region forward, so long as the
-                                * first changed line matches the following unchanged one.
-                                * This merges with following changed regions.
-                                * Do this second, so that if there are no merges,
-                                * the changed region is moved forward as far as possible.
-                                */
-                               while ( $i < $len && $lines[$start] == $lines[$i] ) {
-                                       $changed[$start++] = false;
-                                       $changed[$i++] = 1;
-                                       while ( $i < $len && $changed[$i] ) {
-                                               $i++;
-                                       }
-
-                                       assert( $j < $other_len && ! $other_changed[$j] );
-                                       $j++;
-                                       if ( $j < $other_len && $other_changed[$j] ) {
-                                               $corresponding = $i;
-                                               while ( $j < $other_len && $other_changed[$j] ) {
-                                                       $j++;
-                                               }
-                                       }
-                               }
-                       } while ( $runlength != $i - $start );
-
-                       /*
-                        * If possible, move the fully-merged run of changes
-                        * back to a corresponding run in the other file.
-                        */
-                       while ( $corresponding < $i ) {
-                               $changed[--$start] = 1;
-                               $changed[--$i] = 0;
-                               assert( $j > 0 );
-                               while ( $other_changed[--$j] ) {
-                                       continue;
-                               }
-                               assert( $j >= 0 && !$other_changed[$j] );
-                       }
-               }
-       }
-}
-
 /**
  * Class representing a 'diff' between two sequences of strings.
  * @todo document
@@ -559,236 +321,7 @@ class Diff {
 }
 
 /**
- * @todo document, bad name.
- * @private
- * @ingroup DifferenceEngine
- */
-class MappedDiff extends Diff {
-       /**
-        * Constructor.
-        *
-        * Computes diff between sequences of strings.
-        *
-        * This can be used to compute things like
-        * case-insensitve diffs, or diffs which ignore
-        * changes in white-space.
-        *
-        * @param string[] $from_lines An array of strings.
-        *   Typically these are lines from a file.
-        * @param string[] $to_lines An array of strings.
-        * @param string[] $mapped_from_lines This array should
-        *   have the same size number of elements as $from_lines.
-        *   The elements in $mapped_from_lines and
-        *   $mapped_to_lines are what is actually compared
-        *   when computing the diff.
-        * @param string[] $mapped_to_lines This array should
-        *   have the same number of elements as $to_lines.
-        */
-       public function __construct( $from_lines, $to_lines,
-               $mapped_from_lines, $mapped_to_lines ) {
-
-               assert( count( $from_lines ) == count( $mapped_from_lines ) );
-               assert( count( $to_lines ) == count( $mapped_to_lines ) );
-
-               parent::__construct( $mapped_from_lines, $mapped_to_lines );
-
-               $xi = $yi = 0;
-               $editCount = count( $this->edits );
-               for ( $i = 0; $i < $editCount; $i++ ) {
-                       $orig = &$this->edits[$i]->orig;
-                       if ( is_array( $orig ) ) {
-                               $orig = array_slice( $from_lines, $xi, count( $orig ) );
-                               $xi += count( $orig );
-                       }
-
-                       $closing = &$this->edits[$i]->closing;
-                       if ( is_array( $closing ) ) {
-                               $closing = array_slice( $to_lines, $yi, count( $closing ) );
-                               $yi += count( $closing );
-                       }
-               }
-       }
-}
-
-/**
- * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ * @deprecated Alias for WordAccumulator, to be soon removed
  */
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class HWLDFWordAccumulator {
-       public $insClass = ' class="diffchange diffchange-inline"';
-       public $delClass = ' class="diffchange diffchange-inline"';
-
-       private $lines = [];
-       private $line = '';
-       private $group = '';
-       private $tag = '';
-
-       /**
-        * @param string $new_tag
-        */
-       private function flushGroup( $new_tag ) {
-               if ( $this->group !== '' ) {
-                       if ( $this->tag == 'ins' ) {
-                               $this->line .= "<ins{$this->insClass}>" .
-                                       htmlspecialchars( $this->group ) . '</ins>';
-                       } elseif ( $this->tag == 'del' ) {
-                               $this->line .= "<del{$this->delClass}>" .
-                                       htmlspecialchars( $this->group ) . '</del>';
-                       } else {
-                               $this->line .= htmlspecialchars( $this->group );
-                       }
-               }
-               $this->group = '';
-               $this->tag = $new_tag;
-       }
-
-       /**
-        * @param string $new_tag
-        */
-       private function flushLine( $new_tag ) {
-               $this->flushGroup( $new_tag );
-               if ( $this->line != '' ) {
-                       array_push( $this->lines, $this->line );
-               } else {
-                       # make empty lines visible by inserting an NBSP
-                       array_push( $this->lines, '&#160;' );
-               }
-               $this->line = '';
-       }
-
-       /**
-        * @param string[] $words
-        * @param string $tag
-        */
-       public function addWords( $words, $tag = '' ) {
-               if ( $tag != $this->tag ) {
-                       $this->flushGroup( $tag );
-               }
-
-               foreach ( $words as $word ) {
-                       // new-line should only come as first char of word.
-                       if ( $word == '' ) {
-                               continue;
-                       }
-                       if ( $word[0] == "\n" ) {
-                               $this->flushLine( $tag );
-                               $word = substr( $word, 1 );
-                       }
-                       assert( !strstr( $word, "\n" ) );
-                       $this->group .= $word;
-               }
-       }
-
-       /**
-        * @return string[]
-        */
-       public function getLines() {
-               $this->flushLine( '~done' );
-
-               return $this->lines;
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class WordLevelDiff extends MappedDiff {
-       const MAX_LINE_LENGTH = 10000;
-
-       /**
-        * @param string[] $orig_lines
-        * @param string[] $closing_lines
-        */
-       public function __construct( $orig_lines, $closing_lines ) {
-
-               list( $orig_words, $orig_stripped ) = $this->split( $orig_lines );
-               list( $closing_words, $closing_stripped ) = $this->split( $closing_lines );
-
-               parent::__construct( $orig_words, $closing_words,
-                       $orig_stripped, $closing_stripped );
-       }
-
-       /**
-        * @param string[] $lines
-        *
-        * @return array[]
-        */
-       private function split( $lines ) {
-
-               $words = [];
-               $stripped = [];
-               $first = true;
-               foreach ( $lines as $line ) {
-                       # If the line is too long, just pretend the entire line is one big word
-                       # This prevents resource exhaustion problems
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $words[] = "\n";
-                               $stripped[] = "\n";
-                       }
-                       if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
-                               $words[] = $line;
-                               $stripped[] = $line;
-                       } else {
-                               $m = [];
-                               if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
-                                       $line, $m )
-                               ) {
-                                       foreach ( $m[0] as $word ) {
-                                               $words[] = $word;
-                                       }
-                                       foreach ( $m[1] as $stripped_word ) {
-                                               $stripped[] = $stripped_word;
-                                       }
-                               }
-                       }
-               }
-
-               return [ $words, $stripped ];
-       }
-
-       /**
-        * @return string[]
-        */
-       public function orig() {
-               $orig = new HWLDFWordAccumulator;
-
-               foreach ( $this->edits as $edit ) {
-                       if ( $edit->type == 'copy' ) {
-                               $orig->addWords( $edit->orig );
-                       } elseif ( $edit->orig ) {
-                               $orig->addWords( $edit->orig, 'del' );
-                       }
-               }
-               $lines = $orig->getLines();
-
-               return $lines;
-       }
-
-       /**
-        * @return string[]
-        */
-       public function closing() {
-               $closing = new HWLDFWordAccumulator;
-
-               foreach ( $this->edits as $edit ) {
-                       if ( $edit->type == 'copy' ) {
-                               $closing->addWords( $edit->closing );
-                       } elseif ( $edit->closing ) {
-                               $closing->addWords( $edit->closing, 'ins' );
-                       }
-               }
-               $lines = $closing->getLines();
-
-               return $lines;
-       }
-
+class HWLDFWordAccumulator extends MediaWiki\Diff\WordAccumulator {
 }
diff --git a/includes/diff/DiffEngine.php b/includes/diff/DiffEngine.php
new file mode 100644 (file)
index 0000000..1853b86
--- /dev/null
@@ -0,0 +1,825 @@
+<?php
+/**
+ * New version of the difference engine
+ *
+ * Copyright © 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * 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 DifferenceEngine
+ */
+
+/**
+ * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
+ * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
+ * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
+ * "An O(NP) Sequence Comparison Algorithm").
+ *
+ * This implementation supports an upper bound on the execution time.
+ *
+ * Some ideas (and a bit of code) are from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *     ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
+ *
+ * @author Guy Van den Broeck, Geoffrey T. Dairiki, Tim Starling
+ * @ingroup DifferenceEngine
+ */
+class DiffEngine {
+
+       // Input variables
+       private $from;
+       private $to;
+       private $m;
+       private $n;
+
+       private $tooLong;
+       private $powLimit;
+
+       // State variables
+       private $maxDifferences;
+       private $lcsLengthCorrectedForHeuristic = false;
+
+       // Output variables
+       public $length;
+       public $removed;
+       public $added;
+       public $heuristicUsed;
+
+       function __construct( $tooLong = 2000000, $powLimit = 1.45 ) {
+               $this->tooLong = $tooLong;
+               $this->powLimit = $powLimit;
+       }
+
+       /**
+        * Performs diff
+        *
+        * @param string[] $from_lines
+        * @param string[] $to_lines
+        *
+        * @return DiffOp[]
+        */
+       public function diff( $from_lines, $to_lines ) {
+
+               // Diff and store locally
+               $this->diffInternal( $from_lines, $to_lines );
+
+               // Merge edits when possible
+               $this->shiftBoundaries( $from_lines, $this->removed, $this->added );
+               $this->shiftBoundaries( $to_lines, $this->added, $this->removed );
+
+               // Compute the edit operations.
+               $n_from = count( $from_lines );
+               $n_to = count( $to_lines );
+
+               $edits = [];
+               $xi = $yi = 0;
+               while ( $xi < $n_from || $yi < $n_to ) {
+                       assert( $yi < $n_to || $this->removed[$xi] );
+                       assert( $xi < $n_from || $this->added[$yi] );
+
+                       // Skip matching "snake".
+                       $copy = [];
+                       while ( $xi < $n_from && $yi < $n_to
+                                       && !$this->removed[$xi] && !$this->added[$yi]
+                       ) {
+                               $copy[] = $from_lines[$xi++];
+                               ++$yi;
+                       }
+                       if ( $copy ) {
+                               $edits[] = new DiffOpCopy( $copy );
+                       }
+
+                       // Find deletes & adds.
+                       $delete = [];
+                       while ( $xi < $n_from && $this->removed[$xi] ) {
+                               $delete[] = $from_lines[$xi++];
+                       }
+
+                       $add = [];
+                       while ( $yi < $n_to && $this->added[$yi] ) {
+                               $add[] = $to_lines[$yi++];
+                       }
+
+                       if ( $delete && $add ) {
+                               $edits[] = new DiffOpChange( $delete, $add );
+                       } elseif ( $delete ) {
+                               $edits[] = new DiffOpDelete( $delete );
+                       } elseif ( $add ) {
+                               $edits[] = new DiffOpAdd( $add );
+                       }
+               }
+
+               return $edits;
+       }
+
+       /**
+        * Adjust inserts/deletes of identical lines to join changes
+        * as much as possible.
+        *
+        * We do something when a run of changed lines include a
+        * line at one end and has an excluded, identical line at the other.
+        * We are free to choose which identical line is included.
+        * `compareseq' usually chooses the one at the beginning,
+        * but usually it is cleaner to consider the following identical line
+        * to be the "change".
+        *
+        * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+        *
+        * @param string[] $lines
+        * @param string[] $changed
+        * @param string[] $other_changed
+        */
+       private function shiftBoundaries( array $lines, array &$changed, array $other_changed ) {
+               $i = 0;
+               $j = 0;
+
+               assert( count( $lines ) == count( $changed ) );
+               $len = count( $lines );
+               $other_len = count( $other_changed );
+
+               while ( 1 ) {
+                       /*
+                        * Scan forwards to find beginning of another run of changes.
+                        * Also keep track of the corresponding point in the other file.
+                        *
+                        * Throughout this code, $i and $j are adjusted together so that
+                        * the first $i elements of $changed and the first $j elements
+                        * of $other_changed both contain the same number of zeros
+                        * (unchanged lines).
+                        * Furthermore, $j is always kept so that $j == $other_len or
+                        * $other_changed[$j] == false.
+                        */
+                       while ( $j < $other_len && $other_changed[$j] ) {
+                               $j++;
+                       }
+
+                       while ( $i < $len && !$changed[$i] ) {
+                               assert( $j < $other_len && ! $other_changed[$j] );
+                               $i++;
+                               $j++;
+                               while ( $j < $other_len && $other_changed[$j] ) {
+                                       $j++;
+                               }
+                       }
+
+                       if ( $i == $len ) {
+                               break;
+                       }
+
+                       $start = $i;
+
+                       // Find the end of this run of changes.
+                       while ( ++$i < $len && $changed[$i] ) {
+                               continue;
+                       }
+
+                       do {
+                               /*
+                                * Record the length of this run of changes, so that
+                                * we can later determine whether the run has grown.
+                                */
+                               $runlength = $i - $start;
+
+                               /*
+                                * Move the changed region back, so long as the
+                                * previous unchanged line matches the last changed one.
+                                * This merges with previous changed regions.
+                                */
+                               while ( $start > 0 && $lines[$start - 1] == $lines[$i - 1] ) {
+                                       $changed[--$start] = 1;
+                                       $changed[--$i] = false;
+                                       while ( $start > 0 && $changed[$start - 1] ) {
+                                               $start--;
+                                       }
+                                       assert( $j > 0 );
+                                       while ( $other_changed[--$j] ) {
+                                               continue;
+                                       }
+                                       assert( $j >= 0 && !$other_changed[$j] );
+                               }
+
+                               /*
+                                * Set CORRESPONDING to the end of the changed run, at the last
+                                * point where it corresponds to a changed run in the other file.
+                                * CORRESPONDING == LEN means no such point has been found.
+                                */
+                               $corresponding = $j < $other_len ? $i : $len;
+
+                               /*
+                                * Move the changed region forward, so long as the
+                                * first changed line matches the following unchanged one.
+                                * This merges with following changed regions.
+                                * Do this second, so that if there are no merges,
+                                * the changed region is moved forward as far as possible.
+                                */
+                               while ( $i < $len && $lines[$start] == $lines[$i] ) {
+                                       $changed[$start++] = false;
+                                       $changed[$i++] = 1;
+                                       while ( $i < $len && $changed[$i] ) {
+                                               $i++;
+                                       }
+
+                                       assert( $j < $other_len && ! $other_changed[$j] );
+                                       $j++;
+                                       if ( $j < $other_len && $other_changed[$j] ) {
+                                               $corresponding = $i;
+                                               while ( $j < $other_len && $other_changed[$j] ) {
+                                                       $j++;
+                                               }
+                                       }
+                               }
+                       } while ( $runlength != $i - $start );
+
+                       /*
+                        * If possible, move the fully-merged run of changes
+                        * back to a corresponding run in the other file.
+                        */
+                       while ( $corresponding < $i ) {
+                               $changed[--$start] = 1;
+                               $changed[--$i] = 0;
+                               assert( $j > 0 );
+                               while ( $other_changed[--$j] ) {
+                                       continue;
+                               }
+                               assert( $j >= 0 && !$other_changed[$j] );
+                       }
+               }
+       }
+
+       /**
+        * @param string[] $from
+        * @param string[] $to
+        */
+       protected function diffInternal( array $from, array $to ) {
+               // remember initial lengths
+               $m = count( $from );
+               $n = count( $to );
+
+               $this->heuristicUsed = false;
+
+               // output
+               $removed = $m > 0 ? array_fill( 0, $m, true ) : [];
+               $added = $n > 0 ? array_fill( 0, $n, true ) : [];
+
+               // reduce the complexity for the next step (intentionally done twice)
+               // remove common tokens at the start
+               $i = 0;
+               while ( $i < $m && $i < $n && $from[$i] === $to[$i] ) {
+                       $removed[$i] = $added[$i] = false;
+                       unset( $from[$i], $to[$i] );
+                       ++$i;
+               }
+
+               // remove common tokens at the end
+               $j = 1;
+               while ( $i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j] ) {
+                       $removed[$m - $j] = $added[$n - $j] = false;
+                       unset( $from[$m - $j], $to[$n - $j] );
+                       ++$j;
+               }
+
+               $this->from = $newFromIndex = $this->to = $newToIndex = [];
+
+               // remove tokens not in both sequences
+               $shared = [];
+               foreach ( $from as $key ) {
+                       $shared[$key] = false;
+               }
+
+               foreach ( $to as $index => &$el ) {
+                       if ( array_key_exists( $el, $shared ) ) {
+                               // keep it
+                               $this->to[] = $el;
+                               $shared[$el] = true;
+                               $newToIndex[] = $index;
+                       }
+               }
+               foreach ( $from as $index => &$el ) {
+                       if ( $shared[$el] ) {
+                               // keep it
+                               $this->from[] = $el;
+                               $newFromIndex[] = $index;
+                       }
+               }
+
+               unset( $shared, $from, $to );
+
+               $this->m = count( $this->from );
+               $this->n = count( $this->to );
+
+               $this->removed = $this->m > 0 ? array_fill( 0, $this->m, true ) : [];
+               $this->added = $this->n > 0 ? array_fill( 0, $this->n, true ) : [];
+
+               if ( $this->m == 0 || $this->n == 0 ) {
+                       $this->length = 0;
+               } else {
+                       $this->maxDifferences = ceil( ( $this->m + $this->n ) / 2.0 );
+                       if ( $this->m * $this->n > $this->tooLong ) {
+                               // limit complexity to D^POW_LIMIT for long sequences
+                               $this->maxDifferences = floor( pow( $this->maxDifferences, $this->powLimit - 1.0 ) );
+                               wfDebug( "Limiting max number of differences to $this->maxDifferences\n" );
+                       }
+
+                       /*
+                        * The common prefixes and suffixes are always part of some LCS, include
+                        * them now to reduce our search space
+                        */
+                       $max = min( $this->m, $this->n );
+                       for ( $forwardBound = 0; $forwardBound < $max
+                               && $this->from[$forwardBound] === $this->to[$forwardBound];
+                               ++$forwardBound
+                       ) {
+                               $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
+                       }
+
+                       $backBoundL1 = $this->m - 1;
+                       $backBoundL2 = $this->n - 1;
+
+                       while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
+                               && $this->from[$backBoundL1] === $this->to[$backBoundL2]
+                       ) {
+                               $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
+                       }
+
+                       $temp = array_fill( 0, $this->m + $this->n + 1, 0 );
+                       $V = [ $temp, $temp ];
+                       $snake = [ 0, 0, 0 ];
+
+                       $this->length = $forwardBound + $this->m - $backBoundL1 - 1
+                               + $this->lcs_rec(
+                                       $forwardBound,
+                                       $backBoundL1,
+                                       $forwardBound,
+                                       $backBoundL2,
+                                       $V,
+                                       $snake
+                       );
+               }
+
+               $this->m = $m;
+               $this->n = $n;
+
+               $this->length += $i + $j - 1;
+
+               foreach ( $this->removed as $key => &$removed_elem ) {
+                       if ( !$removed_elem ) {
+                               $removed[$newFromIndex[$key]] = false;
+                       }
+               }
+               foreach ( $this->added as $key => &$added_elem ) {
+                       if ( !$added_elem ) {
+                               $added[$newToIndex[$key]] = false;
+                       }
+               }
+               $this->removed = $removed;
+               $this->added = $added;
+       }
+
+       function diff_range( $from_lines, $to_lines ) {
+               // Diff and store locally
+               $this->diff( $from_lines, $to_lines );
+               unset( $from_lines, $to_lines );
+
+               $ranges = [];
+               $xi = $yi = 0;
+               while ( $xi < $this->m || $yi < $this->n ) {
+                       // Matching "snake".
+                       while ( $xi < $this->m && $yi < $this->n
+                               && !$this->removed[$xi]
+                               && !$this->added[$yi]
+                       ) {
+                               ++$xi;
+                               ++$yi;
+                       }
+                       // Find deletes & adds.
+                       $xstart = $xi;
+                       while ( $xi < $this->m && $this->removed[$xi] ) {
+                               ++$xi;
+                       }
+
+                       $ystart = $yi;
+                       while ( $yi < $this->n && $this->added[$yi] ) {
+                               ++$yi;
+                       }
+
+                       if ( $xi > $xstart || $yi > $ystart ) {
+                               $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi );
+                       }
+               }
+
+               return $ranges;
+       }
+
+       private function lcs_rec( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
+               // check that both sequences are non-empty
+               if ( $bottoml1 > $topl1 || $bottoml2 > $topl2 ) {
+                       return 0;
+               }
+
+               $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2,
+                       $topl2, $V, $snake );
+
+               // need to store these so we don't lose them when they're
+               // overwritten by the recursion
+               $len = $snake[2];
+               $startx = $snake[0];
+               $starty = $snake[1];
+
+               // the middle snake is part of the LCS, store it
+               for ( $i = 0; $i < $len; ++$i ) {
+                       $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
+               }
+
+               if ( $d > 1 ) {
+                       return $len
+                       + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2,
+                               $starty - 1, $V, $snake )
+                       + $this->lcs_rec( $startx + $len, $topl1, $starty + $len,
+                               $topl2, $V, $snake );
+               } elseif ( $d == 1 ) {
+                       /*
+                        * In this case the sequences differ by exactly 1 line. We have
+                        * already saved all the lines after the difference in the for loop
+                        * above, now we need to save all the lines before the difference.
+                        */
+                       $max = min( $startx - $bottoml1, $starty - $bottoml2 );
+                       for ( $i = 0; $i < $max; ++$i ) {
+                               $this->removed[$bottoml1 + $i] =
+                                       $this->added[$bottoml2 + $i] = false;
+                       }
+
+                       return $max + $len;
+               }
+
+               return $len;
+       }
+
+       private function find_middle_snake( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
+               $from = &$this->from;
+               $to = &$this->to;
+               $V0 = &$V[0];
+               $V1 = &$V[1];
+               $snake0 = &$snake[0];
+               $snake1 = &$snake[1];
+               $snake2 = &$snake[2];
+               $bottoml1_min_1 = $bottoml1 - 1;
+               $bottoml2_min_1 = $bottoml2 - 1;
+               $N = $topl1 - $bottoml1_min_1;
+               $M = $topl2 - $bottoml2_min_1;
+               $delta = $N - $M;
+               $maxabsx = $N + $bottoml1;
+               $maxabsy = $M + $bottoml2;
+               $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
+
+               // value_to_add_forward: a 0 or 1 that we add to the start
+               // offset to make it odd/even
+               if ( ( $M & 1 ) == 1 ) {
+                       $value_to_add_forward = 1;
+               } else {
+                       $value_to_add_forward = 0;
+               }
+
+               if ( ( $N & 1 ) == 1 ) {
+                       $value_to_add_backward = 1;
+               } else {
+                       $value_to_add_backward = 0;
+               }
+
+               $start_forward = -$M;
+               $end_forward = $N;
+               $start_backward = -$N;
+               $end_backward = $M;
+
+               $limit_min_1 = $limit - 1;
+               $limit_plus_1 = $limit + 1;
+
+               $V0[$limit_plus_1] = 0;
+               $V1[$limit_min_1] = $N;
+               $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
+
+               if ( ( $delta & 1 ) == 1 ) {
+                       for ( $d = 0; $d <= $limit; ++$d ) {
+                               $start_diag = max( $value_to_add_forward + $start_forward, -$d );
+                               $end_diag = min( $end_forward, $d );
+                               $value_to_add_forward = 1 - $value_to_add_forward;
+
+                               // compute forward furthest reaching paths
+                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
+                                       if ( $k == -$d || ( $k < $d
+                                                       && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
+                                       ) {
+                                               $x = $V0[$limit_plus_1 + $k];
+                                       } else {
+                                               $x = $V0[$limit_min_1 + $k] + 1;
+                                       }
+
+                                       $absx = $snake0 = $x + $bottoml1;
+                                       $absy = $snake1 = $x - $k + $bottoml2;
+
+                                       while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
+                                               ++$absx;
+                                               ++$absy;
+                                       }
+                                       $x = $absx - $bottoml1;
+
+                                       $snake2 = $absx - $snake0;
+                                       $V0[$limit + $k] = $x;
+                                       if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1
+                                               && $x >= $V1[$limit + $k - $delta]
+                                       ) {
+                                               return 2 * $d - 1;
+                                       }
+
+                                       // check to see if we can cut down the diagonal range
+                                       if ( $x >= $N && $end_forward > $k - 1 ) {
+                                               $end_forward = $k - 1;
+                                       } elseif ( $absy - $bottoml2 >= $M ) {
+                                               $start_forward = $k + 1;
+                                               $value_to_add_forward = 0;
+                                       }
+                               }
+
+                               $start_diag = max( $value_to_add_backward + $start_backward, -$d );
+                               $end_diag = min( $end_backward, $d );
+                               $value_to_add_backward = 1 - $value_to_add_backward;
+
+                               // compute backward furthest reaching paths
+                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
+                                       if ( $k == $d
+                                               || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
+                                       ) {
+                                               $x = $V1[$limit_min_1 + $k];
+                                       } else {
+                                               $x = $V1[$limit_plus_1 + $k] - 1;
+                                       }
+
+                                       $y = $x - $k - $delta;
+
+                                       $snake2 = 0;
+                                       while ( $x > 0 && $y > 0
+                                               && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
+                                       ) {
+                                               --$x;
+                                               --$y;
+                                               ++$snake2;
+                                       }
+                                       $V1[$limit + $k] = $x;
+
+                                       // check to see if we can cut down our diagonal range
+                                       if ( $x <= 0 ) {
+                                               $start_backward = $k + 1;
+                                               $value_to_add_backward = 0;
+                                       } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
+                                               $end_backward = $k - 1;
+                                       }
+                               }
+                       }
+               } else {
+                       for ( $d = 0; $d <= $limit; ++$d ) {
+                               $start_diag = max( $value_to_add_forward + $start_forward, -$d );
+                               $end_diag = min( $end_forward, $d );
+                               $value_to_add_forward = 1 - $value_to_add_forward;
+
+                               // compute forward furthest reaching paths
+                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
+                                       if ( $k == -$d
+                                               || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
+                                       ) {
+                                               $x = $V0[$limit_plus_1 + $k];
+                                       } else {
+                                               $x = $V0[$limit_min_1 + $k] + 1;
+                                       }
+
+                                       $absx = $snake0 = $x + $bottoml1;
+                                       $absy = $snake1 = $x - $k + $bottoml2;
+
+                                       while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
+                                               ++$absx;
+                                               ++$absy;
+                                       }
+                                       $x = $absx - $bottoml1;
+                                       $snake2 = $absx - $snake0;
+                                       $V0[$limit + $k] = $x;
+
+                                       // check to see if we can cut down the diagonal range
+                                       if ( $x >= $N && $end_forward > $k - 1 ) {
+                                               $end_forward = $k - 1;
+                                       } elseif ( $absy - $bottoml2 >= $M ) {
+                                               $start_forward = $k + 1;
+                                               $value_to_add_forward = 0;
+                                       }
+                               }
+
+                               $start_diag = max( $value_to_add_backward + $start_backward, -$d );
+                               $end_diag = min( $end_backward, $d );
+                               $value_to_add_backward = 1 - $value_to_add_backward;
+
+                               // compute backward furthest reaching paths
+                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
+                                       if ( $k == $d
+                                               || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
+                                       ) {
+                                               $x = $V1[$limit_min_1 + $k];
+                                       } else {
+                                               $x = $V1[$limit_plus_1 + $k] - 1;
+                                       }
+
+                                       $y = $x - $k - $delta;
+
+                                       $snake2 = 0;
+                                       while ( $x > 0 && $y > 0
+                                               && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
+                                       ) {
+                                               --$x;
+                                               --$y;
+                                               ++$snake2;
+                                       }
+                                       $V1[$limit + $k] = $x;
+
+                                       if ( $k >= -$delta - $d && $k <= $d - $delta
+                                               && $x <= $V0[$limit + $k + $delta]
+                                       ) {
+                                               $snake0 = $bottoml1 + $x;
+                                               $snake1 = $bottoml2 + $y;
+
+                                               return 2 * $d;
+                                       }
+
+                                       // check to see if we can cut down our diagonal range
+                                       if ( $x <= 0 ) {
+                                               $start_backward = $k + 1;
+                                               $value_to_add_backward = 0;
+                                       } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
+                                               $end_backward = $k - 1;
+                                       }
+                               }
+                       }
+               }
+               /*
+                * computing the true LCS is too expensive, instead find the diagonal
+                * with the most progress and pretend a midle snake of length 0 occurs
+                * there.
+                */
+
+               $most_progress = self::findMostProgress( $M, $N, $limit, $V );
+
+               $snake0 = $bottoml1 + $most_progress[0];
+               $snake1 = $bottoml2 + $most_progress[1];
+               $snake2 = 0;
+               wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" );
+               $this->heuristicUsed = true;
+
+               return 5; /*
+               * HACK: since we didn't really finish the LCS computation
+               * we don't really know the length of the SES. We don't do
+               * anything with the result anyway, unless it's <=1. We know
+               * for a fact SES > 1 so 5 is as good a number as any to
+               * return here
+               */
+       }
+
+       private static function findMostProgress( $M, $N, $limit, $V ) {
+               $delta = $N - $M;
+
+               if ( ( $M & 1 ) == ( $limit & 1 ) ) {
+                       $forward_start_diag = max( -$M, -$limit );
+               } else {
+                       $forward_start_diag = max( 1 - $M, -$limit );
+               }
+
+               $forward_end_diag = min( $N, $limit );
+
+               if ( ( $N & 1 ) == ( $limit & 1 ) ) {
+                       $backward_start_diag = max( -$N, -$limit );
+               } else {
+                       $backward_start_diag = max( 1 - $N, -$limit );
+               }
+
+               $backward_end_diag = -min( $M, $limit );
+
+               $temp = [ 0, 0, 0 ];
+
+               $max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag,
+                               $backward_end_diag - $backward_start_diag ) / 2 ), $temp );
+               $num_progress = 0; // the 1st entry is current, it is initialized
+               // with 0s
+
+               // first search the forward diagonals
+               for ( $k = $forward_start_diag; $k <= $forward_end_diag; $k += 2 ) {
+                       $x = $V[0][$limit + $k];
+                       $y = $x - $k;
+                       if ( $x > $N || $y > $M ) {
+                               continue;
+                       }
+
+                       $progress = $x + $y;
+                       if ( $progress > $max_progress[0][2] ) {
+                               $num_progress = 0;
+                               $max_progress[0][0] = $x;
+                               $max_progress[0][1] = $y;
+                               $max_progress[0][2] = $progress;
+                       } elseif ( $progress == $max_progress[0][2] ) {
+                               ++$num_progress;
+                               $max_progress[$num_progress][0] = $x;
+                               $max_progress[$num_progress][1] = $y;
+                               $max_progress[$num_progress][2] = $progress;
+                       }
+               }
+
+               $max_progress_forward = true; // initially the maximum
+               // progress is in the forward
+               // direction
+
+               // now search the backward diagonals
+               for ( $k = $backward_start_diag; $k <= $backward_end_diag; $k += 2 ) {
+                       $x = $V[1][$limit + $k];
+                       $y = $x - $k - $delta;
+                       if ( $x < 0 || $y < 0 ) {
+                               continue;
+                       }
+
+                       $progress = $N - $x + $M - $y;
+                       if ( $progress > $max_progress[0][2] ) {
+                               $num_progress = 0;
+                               $max_progress_forward = false;
+                               $max_progress[0][0] = $x;
+                               $max_progress[0][1] = $y;
+                               $max_progress[0][2] = $progress;
+                       } elseif ( $progress == $max_progress[0][2] && !$max_progress_forward ) {
+                               ++$num_progress;
+                               $max_progress[$num_progress][0] = $x;
+                               $max_progress[$num_progress][1] = $y;
+                               $max_progress[$num_progress][2] = $progress;
+                       }
+               }
+
+               // return the middle diagonal with maximal progress.
+               return $max_progress[(int)floor( $num_progress / 2 )];
+       }
+
+       /**
+        * @return mixed
+        */
+       public function getLcsLength() {
+               if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) {
+                       $this->lcsLengthCorrectedForHeuristic = true;
+                       $this->length = $this->m - array_sum( $this->added );
+               }
+
+               return $this->length;
+       }
+
+}
+
+/**
+ * Alternative representation of a set of changes, by the index
+ * ranges that are changed.
+ *
+ * @ingroup DifferenceEngine
+ */
+class RangeDifference {
+
+       /** @var int */
+       public $leftstart;
+
+       /** @var int */
+       public $leftend;
+
+       /** @var int */
+       public $leftlength;
+
+       /** @var int */
+       public $rightstart;
+
+       /** @var int */
+       public $rightend;
+
+       /** @var int */
+       public $rightlength;
+
+       function __construct( $leftstart, $leftend, $rightstart, $rightend ) {
+               $this->leftstart = $leftstart;
+               $this->leftend = $leftend;
+               $this->leftlength = $leftend - $leftstart;
+               $this->rightstart = $rightstart;
+               $this->rightend = $rightend;
+               $this->rightlength = $rightend - $rightstart;
+       }
+
+}
diff --git a/includes/diff/WikiDiff3.php b/includes/diff/WikiDiff3.php
deleted file mode 100644 (file)
index f35e30f..0000000
+++ /dev/null
@@ -1,621 +0,0 @@
-<?php
-/**
- * New version of the difference engine
- *
- * Copyright © 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * 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 DifferenceEngine
- */
-
-/**
- * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
- * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
- * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
- * "An O(NP) Sequence Comparison Algorithm").
- *
- * This implementation supports an upper bound on the execution time.
- *
- * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
- *
- * @author Guy Van den Broeck
- * @ingroup DifferenceEngine
- */
-class WikiDiff3 {
-
-       // Input variables
-       private $from;
-       private $to;
-       private $m;
-       private $n;
-
-       private $tooLong;
-       private $powLimit;
-
-       // State variables
-       private $maxDifferences;
-       private $lcsLengthCorrectedForHeuristic = false;
-
-       // Output variables
-       public $length;
-       public $removed;
-       public $added;
-       public $heuristicUsed;
-
-       function __construct( $tooLong = 2000000, $powLimit = 1.45 ) {
-               $this->tooLong = $tooLong;
-               $this->powLimit = $powLimit;
-       }
-
-       public function diff( /*array*/ $from, /*array*/ $to ) {
-               // remember initial lengths
-               $m = count( $from );
-               $n = count( $to );
-
-               $this->heuristicUsed = false;
-
-               // output
-               $removed = $m > 0 ? array_fill( 0, $m, true ) : [];
-               $added = $n > 0 ? array_fill( 0, $n, true ) : [];
-
-               // reduce the complexity for the next step (intentionally done twice)
-               // remove common tokens at the start
-               $i = 0;
-               while ( $i < $m && $i < $n && $from[$i] === $to[$i] ) {
-                       $removed[$i] = $added[$i] = false;
-                       unset( $from[$i], $to[$i] );
-                       ++$i;
-               }
-
-               // remove common tokens at the end
-               $j = 1;
-               while ( $i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j] ) {
-                       $removed[$m - $j] = $added[$n - $j] = false;
-                       unset( $from[$m - $j], $to[$n - $j] );
-                       ++$j;
-               }
-
-               $this->from = $newFromIndex = $this->to = $newToIndex = [];
-
-               // remove tokens not in both sequences
-               $shared = [];
-               foreach ( $from as $key ) {
-                       $shared[$key] = false;
-               }
-
-               foreach ( $to as $index => &$el ) {
-                       if ( array_key_exists( $el, $shared ) ) {
-                               // keep it
-                               $this->to[] = $el;
-                               $shared[$el] = true;
-                               $newToIndex[] = $index;
-                       }
-               }
-               foreach ( $from as $index => &$el ) {
-                       if ( $shared[$el] ) {
-                               // keep it
-                               $this->from[] = $el;
-                               $newFromIndex[] = $index;
-                       }
-               }
-
-               unset( $shared, $from, $to );
-
-               $this->m = count( $this->from );
-               $this->n = count( $this->to );
-
-               $this->removed = $this->m > 0 ? array_fill( 0, $this->m, true ) : [];
-               $this->added = $this->n > 0 ? array_fill( 0, $this->n, true ) : [];
-
-               if ( $this->m == 0 || $this->n == 0 ) {
-                       $this->length = 0;
-               } else {
-                       $this->maxDifferences = ceil( ( $this->m + $this->n ) / 2.0 );
-                       if ( $this->m * $this->n > $this->tooLong ) {
-                               // limit complexity to D^POW_LIMIT for long sequences
-                               $this->maxDifferences = floor( pow( $this->maxDifferences, $this->powLimit - 1.0 ) );
-                               wfDebug( "Limiting max number of differences to $this->maxDifferences\n" );
-                       }
-
-                       /*
-                        * The common prefixes and suffixes are always part of some LCS, include
-                        * them now to reduce our search space
-                        */
-                       $max = min( $this->m, $this->n );
-                       for ( $forwardBound = 0; $forwardBound < $max
-                               && $this->from[$forwardBound] === $this->to[$forwardBound];
-                               ++$forwardBound
-                       ) {
-                               $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
-                       }
-
-                       $backBoundL1 = $this->m - 1;
-                       $backBoundL2 = $this->n - 1;
-
-                       while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
-                               && $this->from[$backBoundL1] === $this->to[$backBoundL2]
-                       ) {
-                               $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
-                       }
-
-                       $temp = array_fill( 0, $this->m + $this->n + 1, 0 );
-                       $V = [ $temp, $temp ];
-                       $snake = [ 0, 0, 0 ];
-
-                       $this->length = $forwardBound + $this->m - $backBoundL1 - 1
-                               + $this->lcs_rec(
-                                       $forwardBound,
-                                       $backBoundL1,
-                                       $forwardBound,
-                                       $backBoundL2,
-                                       $V,
-                                       $snake
-                       );
-               }
-
-               $this->m = $m;
-               $this->n = $n;
-
-               $this->length += $i + $j - 1;
-
-               foreach ( $this->removed as $key => &$removed_elem ) {
-                       if ( !$removed_elem ) {
-                               $removed[$newFromIndex[$key]] = false;
-                       }
-               }
-               foreach ( $this->added as $key => &$added_elem ) {
-                       if ( !$added_elem ) {
-                               $added[$newToIndex[$key]] = false;
-                       }
-               }
-               $this->removed = $removed;
-               $this->added = $added;
-       }
-
-       function diff_range( $from_lines, $to_lines ) {
-               // Diff and store locally
-               $this->diff( $from_lines, $to_lines );
-               unset( $from_lines, $to_lines );
-
-               $ranges = [];
-               $xi = $yi = 0;
-               while ( $xi < $this->m || $yi < $this->n ) {
-                       // Matching "snake".
-                       while ( $xi < $this->m && $yi < $this->n
-                               && !$this->removed[$xi]
-                               && !$this->added[$yi]
-                       ) {
-                               ++$xi;
-                               ++$yi;
-                       }
-                       // Find deletes & adds.
-                       $xstart = $xi;
-                       while ( $xi < $this->m && $this->removed[$xi] ) {
-                               ++$xi;
-                       }
-
-                       $ystart = $yi;
-                       while ( $yi < $this->n && $this->added[$yi] ) {
-                               ++$yi;
-                       }
-
-                       if ( $xi > $xstart || $yi > $ystart ) {
-                               $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi );
-                       }
-               }
-
-               return $ranges;
-       }
-
-       private function lcs_rec( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
-               // check that both sequences are non-empty
-               if ( $bottoml1 > $topl1 || $bottoml2 > $topl2 ) {
-                       return 0;
-               }
-
-               $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2,
-                       $topl2, $V, $snake );
-
-               // need to store these so we don't lose them when they're
-               // overwritten by the recursion
-               $len = $snake[2];
-               $startx = $snake[0];
-               $starty = $snake[1];
-
-               // the middle snake is part of the LCS, store it
-               for ( $i = 0; $i < $len; ++$i ) {
-                       $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
-               }
-
-               if ( $d > 1 ) {
-                       return $len
-                       + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2,
-                               $starty - 1, $V, $snake )
-                       + $this->lcs_rec( $startx + $len, $topl1, $starty + $len,
-                               $topl2, $V, $snake );
-               } elseif ( $d == 1 ) {
-                       /*
-                        * In this case the sequences differ by exactly 1 line. We have
-                        * already saved all the lines after the difference in the for loop
-                        * above, now we need to save all the lines before the difference.
-                        */
-                       $max = min( $startx - $bottoml1, $starty - $bottoml2 );
-                       for ( $i = 0; $i < $max; ++$i ) {
-                               $this->removed[$bottoml1 + $i] =
-                                       $this->added[$bottoml2 + $i] = false;
-                       }
-
-                       return $max + $len;
-               }
-
-               return $len;
-       }
-
-       private function find_middle_snake( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
-               $from = &$this->from;
-               $to = &$this->to;
-               $V0 = &$V[0];
-               $V1 = &$V[1];
-               $snake0 = &$snake[0];
-               $snake1 = &$snake[1];
-               $snake2 = &$snake[2];
-               $bottoml1_min_1 = $bottoml1 - 1;
-               $bottoml2_min_1 = $bottoml2 - 1;
-               $N = $topl1 - $bottoml1_min_1;
-               $M = $topl2 - $bottoml2_min_1;
-               $delta = $N - $M;
-               $maxabsx = $N + $bottoml1;
-               $maxabsy = $M + $bottoml2;
-               $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
-
-               // value_to_add_forward: a 0 or 1 that we add to the start
-               // offset to make it odd/even
-               if ( ( $M & 1 ) == 1 ) {
-                       $value_to_add_forward = 1;
-               } else {
-                       $value_to_add_forward = 0;
-               }
-
-               if ( ( $N & 1 ) == 1 ) {
-                       $value_to_add_backward = 1;
-               } else {
-                       $value_to_add_backward = 0;
-               }
-
-               $start_forward = -$M;
-               $end_forward = $N;
-               $start_backward = -$N;
-               $end_backward = $M;
-
-               $limit_min_1 = $limit - 1;
-               $limit_plus_1 = $limit + 1;
-
-               $V0[$limit_plus_1] = 0;
-               $V1[$limit_min_1] = $N;
-               $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
-
-               if ( ( $delta & 1 ) == 1 ) {
-                       for ( $d = 0; $d <= $limit; ++$d ) {
-                               $start_diag = max( $value_to_add_forward + $start_forward, -$d );
-                               $end_diag = min( $end_forward, $d );
-                               $value_to_add_forward = 1 - $value_to_add_forward;
-
-                               // compute forward furthest reaching paths
-                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
-                                       if ( $k == -$d || ( $k < $d
-                                                       && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
-                                       ) {
-                                               $x = $V0[$limit_plus_1 + $k];
-                                       } else {
-                                               $x = $V0[$limit_min_1 + $k] + 1;
-                                       }
-
-                                       $absx = $snake0 = $x + $bottoml1;
-                                       $absy = $snake1 = $x - $k + $bottoml2;
-
-                                       while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
-                                               ++$absx;
-                                               ++$absy;
-                                       }
-                                       $x = $absx - $bottoml1;
-
-                                       $snake2 = $absx - $snake0;
-                                       $V0[$limit + $k] = $x;
-                                       if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1
-                                               && $x >= $V1[$limit + $k - $delta]
-                                       ) {
-                                               return 2 * $d - 1;
-                                       }
-
-                                       // check to see if we can cut down the diagonal range
-                                       if ( $x >= $N && $end_forward > $k - 1 ) {
-                                               $end_forward = $k - 1;
-                                       } elseif ( $absy - $bottoml2 >= $M ) {
-                                               $start_forward = $k + 1;
-                                               $value_to_add_forward = 0;
-                                       }
-                               }
-
-                               $start_diag = max( $value_to_add_backward + $start_backward, -$d );
-                               $end_diag = min( $end_backward, $d );
-                               $value_to_add_backward = 1 - $value_to_add_backward;
-
-                               // compute backward furthest reaching paths
-                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
-                                       if ( $k == $d
-                                               || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
-                                       ) {
-                                               $x = $V1[$limit_min_1 + $k];
-                                       } else {
-                                               $x = $V1[$limit_plus_1 + $k] - 1;
-                                       }
-
-                                       $y = $x - $k - $delta;
-
-                                       $snake2 = 0;
-                                       while ( $x > 0 && $y > 0
-                                               && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
-                                       ) {
-                                               --$x;
-                                               --$y;
-                                               ++$snake2;
-                                       }
-                                       $V1[$limit + $k] = $x;
-
-                                       // check to see if we can cut down our diagonal range
-                                       if ( $x <= 0 ) {
-                                               $start_backward = $k + 1;
-                                               $value_to_add_backward = 0;
-                                       } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
-                                               $end_backward = $k - 1;
-                                       }
-                               }
-                       }
-               } else {
-                       for ( $d = 0; $d <= $limit; ++$d ) {
-                               $start_diag = max( $value_to_add_forward + $start_forward, -$d );
-                               $end_diag = min( $end_forward, $d );
-                               $value_to_add_forward = 1 - $value_to_add_forward;
-
-                               // compute forward furthest reaching paths
-                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
-                                       if ( $k == -$d
-                                               || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
-                                       ) {
-                                               $x = $V0[$limit_plus_1 + $k];
-                                       } else {
-                                               $x = $V0[$limit_min_1 + $k] + 1;
-                                       }
-
-                                       $absx = $snake0 = $x + $bottoml1;
-                                       $absy = $snake1 = $x - $k + $bottoml2;
-
-                                       while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
-                                               ++$absx;
-                                               ++$absy;
-                                       }
-                                       $x = $absx - $bottoml1;
-                                       $snake2 = $absx - $snake0;
-                                       $V0[$limit + $k] = $x;
-
-                                       // check to see if we can cut down the diagonal range
-                                       if ( $x >= $N && $end_forward > $k - 1 ) {
-                                               $end_forward = $k - 1;
-                                       } elseif ( $absy - $bottoml2 >= $M ) {
-                                               $start_forward = $k + 1;
-                                               $value_to_add_forward = 0;
-                                       }
-                               }
-
-                               $start_diag = max( $value_to_add_backward + $start_backward, -$d );
-                               $end_diag = min( $end_backward, $d );
-                               $value_to_add_backward = 1 - $value_to_add_backward;
-
-                               // compute backward furthest reaching paths
-                               for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
-                                       if ( $k == $d
-                                               || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
-                                       ) {
-                                               $x = $V1[$limit_min_1 + $k];
-                                       } else {
-                                               $x = $V1[$limit_plus_1 + $k] - 1;
-                                       }
-
-                                       $y = $x - $k - $delta;
-
-                                       $snake2 = 0;
-                                       while ( $x > 0 && $y > 0
-                                               && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
-                                       ) {
-                                               --$x;
-                                               --$y;
-                                               ++$snake2;
-                                       }
-                                       $V1[$limit + $k] = $x;
-
-                                       if ( $k >= -$delta - $d && $k <= $d - $delta
-                                               && $x <= $V0[$limit + $k + $delta]
-                                       ) {
-                                               $snake0 = $bottoml1 + $x;
-                                               $snake1 = $bottoml2 + $y;
-
-                                               return 2 * $d;
-                                       }
-
-                                       // check to see if we can cut down our diagonal range
-                                       if ( $x <= 0 ) {
-                                               $start_backward = $k + 1;
-                                               $value_to_add_backward = 0;
-                                       } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
-                                               $end_backward = $k - 1;
-                                       }
-                               }
-                       }
-               }
-               /*
-                * computing the true LCS is too expensive, instead find the diagonal
-                * with the most progress and pretend a midle snake of length 0 occurs
-                * there.
-                */
-
-               $most_progress = self::findMostProgress( $M, $N, $limit, $V );
-
-               $snake0 = $bottoml1 + $most_progress[0];
-               $snake1 = $bottoml2 + $most_progress[1];
-               $snake2 = 0;
-               wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" );
-               $this->heuristicUsed = true;
-
-               return 5; /*
-               * HACK: since we didn't really finish the LCS computation
-               * we don't really know the length of the SES. We don't do
-               * anything with the result anyway, unless it's <=1. We know
-               * for a fact SES > 1 so 5 is as good a number as any to
-               * return here
-               */
-       }
-
-       private static function findMostProgress( $M, $N, $limit, $V ) {
-               $delta = $N - $M;
-
-               if ( ( $M & 1 ) == ( $limit & 1 ) ) {
-                       $forward_start_diag = max( -$M, -$limit );
-               } else {
-                       $forward_start_diag = max( 1 - $M, -$limit );
-               }
-
-               $forward_end_diag = min( $N, $limit );
-
-               if ( ( $N & 1 ) == ( $limit & 1 ) ) {
-                       $backward_start_diag = max( -$N, -$limit );
-               } else {
-                       $backward_start_diag = max( 1 - $N, -$limit );
-               }
-
-               $backward_end_diag = -min( $M, $limit );
-
-               $temp = [ 0, 0, 0 ];
-
-               $max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag,
-                               $backward_end_diag - $backward_start_diag ) / 2 ), $temp );
-               $num_progress = 0; // the 1st entry is current, it is initialized
-               // with 0s
-
-               // first search the forward diagonals
-               for ( $k = $forward_start_diag; $k <= $forward_end_diag; $k += 2 ) {
-                       $x = $V[0][$limit + $k];
-                       $y = $x - $k;
-                       if ( $x > $N || $y > $M ) {
-                               continue;
-                       }
-
-                       $progress = $x + $y;
-                       if ( $progress > $max_progress[0][2] ) {
-                               $num_progress = 0;
-                               $max_progress[0][0] = $x;
-                               $max_progress[0][1] = $y;
-                               $max_progress[0][2] = $progress;
-                       } elseif ( $progress == $max_progress[0][2] ) {
-                               ++$num_progress;
-                               $max_progress[$num_progress][0] = $x;
-                               $max_progress[$num_progress][1] = $y;
-                               $max_progress[$num_progress][2] = $progress;
-                       }
-               }
-
-               $max_progress_forward = true; // initially the maximum
-               // progress is in the forward
-               // direction
-
-               // now search the backward diagonals
-               for ( $k = $backward_start_diag; $k <= $backward_end_diag; $k += 2 ) {
-                       $x = $V[1][$limit + $k];
-                       $y = $x - $k - $delta;
-                       if ( $x < 0 || $y < 0 ) {
-                               continue;
-                       }
-
-                       $progress = $N - $x + $M - $y;
-                       if ( $progress > $max_progress[0][2] ) {
-                               $num_progress = 0;
-                               $max_progress_forward = false;
-                               $max_progress[0][0] = $x;
-                               $max_progress[0][1] = $y;
-                               $max_progress[0][2] = $progress;
-                       } elseif ( $progress == $max_progress[0][2] && !$max_progress_forward ) {
-                               ++$num_progress;
-                               $max_progress[$num_progress][0] = $x;
-                               $max_progress[$num_progress][1] = $y;
-                               $max_progress[$num_progress][2] = $progress;
-                       }
-               }
-
-               // return the middle diagonal with maximal progress.
-               return $max_progress[(int)floor( $num_progress / 2 )];
-       }
-
-       /**
-        * @return mixed
-        */
-       public function getLcsLength() {
-               if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) {
-                       $this->lcsLengthCorrectedForHeuristic = true;
-                       $this->length = $this->m - array_sum( $this->added );
-               }
-
-               return $this->length;
-       }
-
-}
-
-/**
- * Alternative representation of a set of changes, by the index
- * ranges that are changed.
- *
- * @ingroup DifferenceEngine
- */
-class RangeDifference {
-
-       /** @var int */
-       public $leftstart;
-
-       /** @var int */
-       public $leftend;
-
-       /** @var int */
-       public $leftlength;
-
-       /** @var int */
-       public $rightstart;
-
-       /** @var int */
-       public $rightend;
-
-       /** @var int */
-       public $rightlength;
-
-       function __construct( $leftstart, $leftend, $rightstart, $rightend ) {
-               $this->leftstart = $leftstart;
-               $this->leftend = $leftend;
-               $this->leftlength = $leftend - $leftstart;
-               $this->rightstart = $rightstart;
-               $this->rightend = $rightend;
-               $this->rightlength = $rightend - $rightstart;
-       }
-
-}
diff --git a/includes/diff/WordAccumulator.php b/includes/diff/WordAccumulator.php
new file mode 100644 (file)
index 0000000..a26775f
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Copyright © 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ *
+ * 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 DifferenceEngine
+ * @defgroup DifferenceEngine DifferenceEngine
+ */
+
+namespace MediaWiki\Diff;
+
+/**
+ * Stores, escapes and formats the results of word-level diff
+ *
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class WordAccumulator {
+       public $insClass = ' class="diffchange diffchange-inline"';
+       public $delClass = ' class="diffchange diffchange-inline"';
+
+       private $lines = [];
+       private $line = '';
+       private $group = '';
+       private $tag = '';
+
+       /**
+        * @param string $new_tag
+        */
+       private function flushGroup( $new_tag ) {
+               if ( $this->group !== '' ) {
+                       if ( $this->tag == 'ins' ) {
+                               $this->line .= "<ins{$this->insClass}>" .
+                                                          htmlspecialchars( $this->group ) . '</ins>';
+                       } elseif ( $this->tag == 'del' ) {
+                               $this->line .= "<del{$this->delClass}>" .
+                                                          htmlspecialchars( $this->group ) . '</del>';
+                       } else {
+                               $this->line .= htmlspecialchars( $this->group );
+                       }
+               }
+               $this->group = '';
+               $this->tag = $new_tag;
+       }
+
+       /**
+        * @param string $new_tag
+        */
+       private function flushLine( $new_tag ) {
+               $this->flushGroup( $new_tag );
+               if ( $this->line != '' ) {
+                       array_push( $this->lines, $this->line );
+               } else {
+                       # make empty lines visible by inserting an NBSP
+                       array_push( $this->lines, '&#160;' );
+               }
+               $this->line = '';
+       }
+
+       /**
+        * @param string[] $words
+        * @param string $tag
+        */
+       public function addWords( $words, $tag = '' ) {
+               if ( $tag != $this->tag ) {
+                       $this->flushGroup( $tag );
+               }
+
+               foreach ( $words as $word ) {
+                       // new-line should only come as first char of word.
+                       if ( $word == '' ) {
+                               continue;
+                       }
+                       if ( $word[0] == "\n" ) {
+                               $this->flushLine( $tag );
+                               $word = substr( $word, 1 );
+                       }
+                       assert( !strstr( $word, "\n" ) );
+                       $this->group .= $word;
+               }
+       }
+
+       /**
+        * @return string[]
+        */
+       public function getLines() {
+               $this->flushLine( '~done' );
+
+               return $this->lines;
+       }
+}
diff --git a/includes/diff/WordLevelDiff.php b/includes/diff/WordLevelDiff.php
new file mode 100644 (file)
index 0000000..12cf376
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Copyright © 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ *
+ * 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 DifferenceEngine
+ * @defgroup DifferenceEngine DifferenceEngine
+ */
+
+use MediaWiki\Diff\WordAccumulator;
+
+/**
+ * Performs a word-level diff on several lines
+ *
+ * @ingroup DifferenceEngine
+ */
+class WordLevelDiff extends \Diff {
+       const MAX_LINE_LENGTH = 10000;
+
+       /**
+        * @param string[] $linesBefore
+        * @param string[] $linesAfter
+        */
+       public function __construct( $linesBefore, $linesAfter ) {
+
+               list( $wordsBefore, $wordsBeforeStripped ) = $this->split( $linesBefore );
+               list( $wordsAfter, $wordsAfterStripped ) = $this->split( $linesAfter );
+
+               parent::__construct( $wordsBeforeStripped, $wordsAfterStripped );
+
+               $xi = $yi = 0;
+               $editCount = count( $this->edits );
+               for ( $i = 0; $i < $editCount; $i++ ) {
+                       $orig = &$this->edits[$i]->orig;
+                       if ( is_array( $orig ) ) {
+                               $orig = array_slice( $wordsBefore, $xi, count( $orig ) );
+                               $xi += count( $orig );
+                       }
+
+                       $closing = &$this->edits[$i]->closing;
+                       if ( is_array( $closing ) ) {
+                               $closing = array_slice( $wordsAfter, $yi, count( $closing ) );
+                               $yi += count( $closing );
+                       }
+               }
+
+       }
+
+       /**
+        * @param string[] $lines
+        *
+        * @return array[]
+        */
+       private function split( $lines ) {
+
+               $words = [];
+               $stripped = [];
+               $first = true;
+               foreach ( $lines as $line ) {
+                       # If the line is too long, just pretend the entire line is one big word
+                       # This prevents resource exhaustion problems
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $words[] = "\n";
+                               $stripped[] = "\n";
+                       }
+                       if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
+                               $words[] = $line;
+                               $stripped[] = $line;
+                       } else {
+                               $m = [];
+                               if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
+                                       $line, $m )
+                               ) {
+                                       foreach ( $m[0] as $word ) {
+                                               $words[] = $word;
+                                       }
+                                       foreach ( $m[1] as $stripped_word ) {
+                                               $stripped[] = $stripped_word;
+                                       }
+                               }
+                       }
+               }
+
+               return [ $words, $stripped ];
+       }
+
+       /**
+        * @return string[]
+        */
+       public function orig() {
+               $orig = new WordAccumulator;
+
+               foreach ( $this->edits as $edit ) {
+                       if ( $edit->type == 'copy' ) {
+                               $orig->addWords( $edit->orig );
+                       } elseif ( $edit->orig ) {
+                               $orig->addWords( $edit->orig, 'del' );
+                       }
+               }
+               $lines = $orig->getLines();
+
+               return $lines;
+       }
+
+       /**
+        * @return string[]
+        */
+       public function closing() {
+               $closing = new WordAccumulator;
+
+               foreach ( $this->edits as $edit ) {
+                       if ( $edit->type == 'copy' ) {
+                               $closing->addWords( $edit->closing );
+                       } elseif ( $edit->closing ) {
+                               $closing->addWords( $edit->closing, 'ins' );
+                       }
+               }
+               $lines = $closing->getLines();
+
+               return $lines;
+       }
+
+}
diff --git a/includes/interwiki/ClassicInterwikiLookup.php b/includes/interwiki/ClassicInterwikiLookup.php
new file mode 100644 (file)
index 0000000..6ac165a
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+namespace MediaWiki\Interwiki;
+
+/**
+ * InterwikiLookup implementing the "classic" interwiki storage (hardcoded up to MW 1.26).
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+use \Cdb\Exception as CdbException;
+use \Cdb\Reader as CdbReader;
+use Database;
+use Hooks;
+use Interwiki;
+use Language;
+use MapCacheLRU;
+use WANObjectCache;
+
+/**
+ * InterwikiLookup implementing the "classic" interwiki storage (hardcoded up to MW 1.26).
+ *
+ * This implements two levels of caching (in-process array and a WANObjectCache)
+ * and tree storage backends (SQL, CDB, and plain PHP arrays).
+ *
+ * All information is loaded on creation when called by $this->fetch( $prefix ).
+ * All work is done on slave, because this should *never* change (except during
+ * schema updates etc, which aren't wiki-related)
+ *
+ * @since 1.28
+ */
+class ClassicInterwikiLookup implements InterwikiLookup {
+
+       /**
+        * @var MapCacheLRU
+        */
+       private $localCache;
+
+       /**
+        * @var Language
+        */
+       private $contentLanguage;
+
+       /**
+        * @var WANObjectCache
+        */
+       private $objectCache;
+
+       /**
+        * @var int
+        */
+       private $objectCacheExpiry;
+
+       /**
+        * @var bool|array|string
+        */
+       private $cdbData;
+
+       /**
+        * @var int
+        */
+       private $interwikiScopes;
+
+       /**
+        * @var string
+        */
+       private $fallbackSite;
+
+       /**
+        * @var CdbReader|null
+        */
+       private $cdbReader = null;
+
+       /**
+        * @var string|null
+        */
+       private $thisSite = null;
+
+       /**
+        * @param Language $contentLanguage Language object used to convert prefixes to lower case
+        * @param WANObjectCache $objectCache Cache for interwiki info retrieved from the database
+        * @param int $objectCacheExpiry Expiry time for $objectCache, in seconds
+        * @param bool|array|string $cdbData The path of a CDB file, or
+        *        an array resembling the contents of a CDB file,
+        *        or false to use the database.
+        * @param int $interwikiScopes Specify number of domains to check for messages:
+        *    - 1: Just local wiki level
+        *    - 2: wiki and global levels
+        *    - 3: site level as well as wiki and global levels
+        * @param string $fallbackSite The code to assume for the local site,
+        */
+       function __construct(
+               Language $contentLanguage,
+               WANObjectCache $objectCache,
+               $objectCacheExpiry,
+               $cdbData,
+               $interwikiScopes,
+               $fallbackSite
+       ) {
+               $this->localCache = new MapCacheLRU( 100 );
+
+               $this->contentLanguage = $contentLanguage;
+               $this->objectCache = $objectCache;
+               $this->objectCacheExpiry = $objectCacheExpiry;
+               $this->cdbData = $cdbData;
+               $this->interwikiScopes = $interwikiScopes;
+               $this->fallbackSite = $fallbackSite;
+       }
+
+       /**
+        * Check whether an interwiki prefix exists
+        *
+        * @param string $prefix Interwiki prefix to use
+        * @return bool Whether it exists
+        */
+       public function isValidInterwiki( $prefix ) {
+               $result = $this->fetch( $prefix );
+
+               return (bool)$result;
+       }
+
+       /**
+        * Fetch an Interwiki object
+        *
+        * @param string $prefix Interwiki prefix to use
+        * @return Interwiki|null|bool
+        */
+       public function fetch( $prefix ) {
+               if ( $prefix == '' ) {
+                       return null;
+               }
+
+               $prefix = $this->contentLanguage->lc( $prefix );
+               if ( $this->localCache->has( $prefix ) ) {
+                       return $this->localCache->get( $prefix );
+               }
+
+               if ( $this->cdbData ) {
+                       $iw = $this->getInterwikiCached( $prefix );
+               } else {
+                       $iw = $this->load( $prefix );
+                       if ( !$iw ) {
+                               $iw = false;
+                       }
+               }
+               $this->localCache->set( $prefix, $iw );
+
+               return $iw;
+       }
+
+       /**
+        * Resets locally cached Interwiki objects. This is intended for use during testing only.
+        * This does not invalidate entries in the persistent cache, as invalidateCache() does.
+        * @since 1.27
+        */
+       public function resetLocalCache() {
+               $this->localCache->clear();
+       }
+
+       /**
+        * Purge the in-process and object cache for an interwiki prefix
+        * @param string $prefix
+        */
+       public function invalidateCache( $prefix ) {
+               $this->localCache->clear( $prefix );
+
+               $key = $this->objectCache->makeKey( 'interwiki', $prefix );
+               $this->objectCache->delete( $key );
+       }
+
+       /**
+        * Fetch interwiki prefix data from local cache in constant database.
+        *
+        * @note More logic is explained in DefaultSettings.
+        *
+        * @param string $prefix Interwiki prefix
+        * @return Interwiki
+        */
+       private function getInterwikiCached( $prefix ) {
+               $value = $this->getInterwikiCacheEntry( $prefix );
+
+               if ( $value ) {
+                       // Split values
+                       list( $local, $url ) = explode( ' ', $value, 2 );
+                       return new Interwiki( $prefix, $url, '', '', (int)$local );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Get entry from interwiki cache
+        *
+        * @note More logic is explained in DefaultSettings.
+        *
+        * @param string $prefix Database key
+        * @return bool|string The interwiki entry or false if not found
+        */
+       private function getInterwikiCacheEntry( $prefix ) {
+               wfDebug( __METHOD__ . "( $prefix )\n" );
+               $value = false;
+               try {
+                       // Resolve site name
+                       if ( $this->interwikiScopes >= 3 && !$this->thisSite ) {
+                               $this->thisSite = $this->getCacheValue( '__sites:' . wfWikiID() );
+                               if ( $this->thisSite == '' ) {
+                                       $this->thisSite = $this->fallbackSite;
+                               }
+                       }
+
+                       $value = $this->getCacheValue( wfMemcKey( $prefix ) );
+                       // Site level
+                       if ( $value == '' && $this->interwikiScopes >= 3 ) {
+                               $value = $this->getCacheValue( "_{$this->thisSite}:{$prefix}" );
+                       }
+                       // Global Level
+                       if ( $value == '' && $this->interwikiScopes >= 2 ) {
+                               $value = $this->getCacheValue( "__global:{$prefix}" );
+                       }
+                       if ( $value == 'undef' ) {
+                               $value = '';
+                       }
+               } catch ( CdbException $e ) {
+                       wfDebug( __METHOD__ . ": CdbException caught, error message was "
+                               . $e->getMessage() );
+               }
+
+               return $value;
+       }
+
+       private function getCacheValue( $key ) {
+               if ( $this->cdbReader === null ) {
+                       if ( is_string( $this->cdbData ) ) {
+                               $this->cdbReader = \Cdb\Reader::open( $this->cdbData );
+                       } elseif ( is_array( $this->cdbData ) ) {
+                               $this->cdbReader = new \Cdb\Reader\Hash( $this->cdbData );
+                       } else {
+                               $this->cdbReader = false;
+                       }
+               }
+
+               if ( $this->cdbReader ) {
+                       return $this->cdbReader->get( $key );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Load the interwiki, trying first memcached then the DB
+        *
+        * @param string $prefix The interwiki prefix
+        * @return Interwiki|bool Interwiki if $prefix is valid, otherwise false
+        */
+       private function load( $prefix ) {
+               $iwData = [];
+               if ( !Hooks::run( 'InterwikiLoadPrefix', [ $prefix, &$iwData ] ) ) {
+                       return $this->loadFromArray( $iwData );
+               }
+
+               if ( is_array( $iwData ) ) {
+                       $iw = $this->loadFromArray( $iwData );
+                       if ( $iw ) {
+                               return $iw; // handled by hook
+                       }
+               }
+
+               $iwData = $this->objectCache->getWithSetCallback(
+                       $this->objectCache->makeKey( 'interwiki', $prefix ),
+                       $this->objectCacheExpiry,
+                       function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
+                               $dbr = wfGetDB( DB_SLAVE ); // TODO: inject LoadBalancer
+
+                               $setOpts += Database::getCacheSetOptions( $dbr );
+
+                               $row = $dbr->selectRow(
+                                       'interwiki',
+                                       ClassicInterwikiLookup::selectFields(),
+                                       [ 'iw_prefix' => $prefix ],
+                                       __METHOD__
+                               );
+
+                               return $row ? (array)$row : '!NONEXISTENT';
+                       }
+               );
+
+               if ( is_array( $iwData ) ) {
+                       return $this->loadFromArray( $iwData ) ?: false;
+               }
+
+               return false;
+       }
+
+       /**
+        * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc)
+        *
+        * @param array $mc Associative array: row from the interwiki table
+        * @return Interwiki|bool Interwiki object or false if $mc['iw_url'] is not set
+        */
+       private function loadFromArray( $mc ) {
+               if ( isset( $mc['iw_url'] ) ) {
+                       $url = $mc['iw_url'];
+                       $local = isset( $mc['iw_local'] ) ? $mc['iw_local'] : 0;
+                       $trans = isset( $mc['iw_trans'] ) ? $mc['iw_trans'] : 0;
+                       $api = isset( $mc['iw_api'] ) ? $mc['iw_api'] : '';
+                       $wikiId = isset( $mc['iw_wikiid'] ) ? $mc['iw_wikiid'] : '';
+
+                       return new Interwiki( null, $url, $api, $wikiId, $local, $trans );
+               }
+
+               return false;
+       }
+
+       /**
+        * Fetch all interwiki prefixes from interwiki cache
+        *
+        * @param null|string $local If not null, limits output to local/non-local interwikis
+        * @return array List of prefixes, where each row is an associative array
+        */
+       private function getAllPrefixesCached( $local ) {
+               wfDebug( __METHOD__ . "()\n" );
+               $data = [];
+               try {
+                       /* Resolve site name */
+                       if ( $this->interwikiScopes >= 3 && !$this->thisSite ) {
+                               $site = $this->getCacheValue( '__sites:' . wfWikiID() );
+
+                               if ( $site == '' ) {
+                                       $this->thisSite = $this->fallbackSite;
+                               } else {
+                                       $this->thisSite = $site;
+                               }
+                       }
+
+                       // List of interwiki sources
+                       $sources = [];
+                       // Global Level
+                       if ( $this->interwikiScopes >= 2 ) {
+                               $sources[] = '__global';
+                       }
+                       // Site level
+                       if ( $this->interwikiScopes >= 3 ) {
+                               $sources[] = '_' . $this->thisSite;
+                       }
+                       $sources[] = wfWikiID();
+
+                       foreach ( $sources as $source ) {
+                               $list = $this->getCacheValue( '__list:' . $source );
+                               foreach ( explode( ' ', $list ) as $iw_prefix ) {
+                                       $row = $this->getCacheValue( "{$source}:{$iw_prefix}" );
+                                       if ( !$row ) {
+                                               continue;
+                                       }
+
+                                       list( $iw_local, $iw_url ) = explode( ' ', $row );
+
+                                       if ( $local !== null && $local != $iw_local ) {
+                                               continue;
+                                       }
+
+                                       $data[$iw_prefix] = [
+                                               'iw_prefix' => $iw_prefix,
+                                               'iw_url' => $iw_url,
+                                               'iw_local' => $iw_local,
+                                       ];
+                               }
+                       }
+               } catch ( CdbException $e ) {
+                       wfDebug( __METHOD__ . ": CdbException caught, error message was "
+                               . $e->getMessage() );
+               }
+
+               ksort( $data );
+
+               return array_values( $data );
+       }
+
+       /**
+        * Fetch all interwiki prefixes from DB
+        *
+        * @param string|null $local If not null, limits output to local/non-local interwikis
+        * @return array[] Interwiki rows
+        */
+       private function getAllPrefixesDB( $local ) {
+               $db = wfGetDB( DB_SLAVE ); // TODO: inject DB LoadBalancer
+
+               $where = [];
+
+               if ( $local !== null ) {
+                       if ( $local == 1 ) {
+                               $where['iw_local'] = 1;
+                       } elseif ( $local == 0 ) {
+                               $where['iw_local'] = 0;
+                       }
+               }
+
+               $res = $db->select( 'interwiki',
+                       $this->selectFields(),
+                       $where, __METHOD__, [ 'ORDER BY' => 'iw_prefix' ]
+               );
+
+               $retval = [];
+               foreach ( $res as $row ) {
+                       $retval[] = (array)$row;
+               }
+
+               return $retval;
+       }
+
+       /**
+        * Returns all interwiki prefixes
+        *
+        * @param string|null $local If set, limits output to local/non-local interwikis
+        * @return array[] Interwiki rows, where each row is an associative array
+        */
+       public function getAllPrefixes( $local = null ) {
+               if ( $this->cdbData ) {
+                       return $this->getAllPrefixesCached( $local );
+               }
+
+               return $this->getAllPrefixesDB( $local );
+       }
+
+       /**
+        * Return the list of interwiki fields that should be selected to create
+        * a new Interwiki object.
+        * @return string[]
+        */
+       private static function selectFields() {
+               return [
+                       'iw_prefix',
+                       'iw_url',
+                       'iw_api',
+                       'iw_wikiid',
+                       'iw_local',
+                       'iw_trans'
+               ];
+       }
+
+}
index 5a0dd36..558e32c 100644 (file)
  *
  * @file
  */
-use \Cdb\Exception as CdbException;
-use \Cdb\Reader as CdbReader;
+use MediaWiki\MediaWikiServices;
 
 /**
- * The interwiki class
- * All information is loaded on creation when called by Interwiki::fetch( $prefix ).
- * All work is done on slave, because this should *never* change (except during
- * schema updates etc, which aren't wiki-related)
+ * Value object for representing interwiki records.
  */
 class Interwiki {
-       // Cache - removes oldest entry when it hits limit
-       protected static $smCache = [];
-       const CACHE_LIMIT = 100; // 0 means unlimited, any other value is max number of entries.
 
        /** @var string The interwiki prefix, (e.g. "Meatball", or the language prefix "de") */
        protected $mPrefix;
@@ -67,335 +60,48 @@ class Interwiki {
        /**
         * Check whether an interwiki prefix exists
         *
+        * @deprecated since 1.28, use InterwikiLookup instead
+        *
         * @param string $prefix Interwiki prefix to use
         * @return bool Whether it exists
         */
        public static function isValidInterwiki( $prefix ) {
-               $result = self::fetch( $prefix );
-
-               return (bool)$result;
+               return MediaWikiServices::getInstance()->getInterwikiLookup()->isValidInterwiki( $prefix );
        }
 
        /**
         * Fetch an Interwiki object
         *
+        * @deprecated since 1.28, use InterwikiLookup instead
+        *
         * @param string $prefix Interwiki prefix to use
         * @return Interwiki|null|bool
         */
        public static function fetch( $prefix ) {
-               global $wgContLang;
-
-               if ( $prefix == '' ) {
-                       return null;
-               }
-
-               $prefix = $wgContLang->lc( $prefix );
-               if ( isset( self::$smCache[$prefix] ) ) {
-                       return self::$smCache[$prefix];
-               }
-
-               global $wgInterwikiCache;
-               if ( $wgInterwikiCache ) {
-                       $iw = Interwiki::getInterwikiCached( $prefix );
-               } else {
-                       $iw = Interwiki::load( $prefix );
-                       if ( !$iw ) {
-                               $iw = false;
-                       }
-               }
-
-               if ( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ) {
-                       reset( self::$smCache );
-                       unset( self::$smCache[key( self::$smCache )] );
-               }
-
-               self::$smCache[$prefix] = $iw;
-
-               return $iw;
-       }
-
-       /**
-        * Resets locally cached Interwiki objects. This is intended for use during testing only.
-        * This does not invalidate entries in the persistent cache, as invalidateCache() does.
-        * @since 1.27
-        */
-       public static function resetLocalCache() {
-               static::$smCache = [];
+               return MediaWikiServices::getInstance()->getInterwikiLookup()->fetch( $prefix );
        }
 
        /**
         * Purge the cache (local and persistent) for an interwiki prefix.
+        *
         * @param string $prefix
         * @since 1.26
         */
        public static function invalidateCache( $prefix ) {
-               $cache = ObjectCache::getMainWANInstance();
-               $key = wfMemcKey( 'interwiki', $prefix );
-               $cache->delete( $key );
-               unset( static::$smCache[$prefix] );
-       }
-
-       /**
-        * Fetch interwiki prefix data from local cache in constant database.
-        *
-        * @note More logic is explained in DefaultSettings.
-        *
-        * @param string $prefix Interwiki prefix
-        * @return Interwiki
-        */
-       protected static function getInterwikiCached( $prefix ) {
-               $value = self::getInterwikiCacheEntry( $prefix );
-
-               $s = new Interwiki( $prefix );
-               if ( $value ) {
-                       // Split values
-                       list( $local, $url ) = explode( ' ', $value, 2 );
-                       $s->mURL = $url;
-                       $s->mLocal = (bool)$local;
-               } else {
-                       $s = false;
-               }
-
-               return $s;
-       }
-
-       /**
-        * Get entry from interwiki cache
-        *
-        * @note More logic is explained in DefaultSettings.
-        *
-        * @param string $prefix Database key
-        * @return bool|string The interwiki entry or false if not found
-        */
-       protected static function getInterwikiCacheEntry( $prefix ) {
-               global $wgInterwikiScopes, $wgInterwikiFallbackSite;
-               static $site;
-
-               $value = false;
-               try {
-                       // Resolve site name
-                       if ( $wgInterwikiScopes >= 3 && !$site ) {
-                               $site = self::getCacheValue( '__sites:' . wfWikiID() );
-                               if ( $site == '' ) {
-                                       $site = $wgInterwikiFallbackSite;
-                               }
-                       }
-
-                       $value = self::getCacheValue( wfMemcKey( $prefix ) );
-                       // Site level
-                       if ( $value == '' && $wgInterwikiScopes >= 3 ) {
-                               $value = self::getCacheValue( "_{$site}:{$prefix}" );
-                       }
-                       // Global Level
-                       if ( $value == '' && $wgInterwikiScopes >= 2 ) {
-                               $value = self::getCacheValue( "__global:{$prefix}" );
-                       }
-                       if ( $value == 'undef' ) {
-                               $value = '';
-                       }
-               } catch ( CdbException $e ) {
-                       wfDebug( __METHOD__ . ": CdbException caught, error message was "
-                               . $e->getMessage() );
-               }
-
-               return $value;
-       }
-
-       private static function getCacheValue( $key ) {
-               global $wgInterwikiCache;
-               static $reader;
-               if ( $reader === null ) {
-                       $reader = is_array( $wgInterwikiCache ) ? false : CdbReader::open( $wgInterwikiCache );
-               }
-               if ( $reader ) {
-                       return $reader->get( $key );
-               } else {
-                       return isset( $wgInterwikiCache[$key] ) ? $wgInterwikiCache[$key] : false;
-               }
-       }
-
-       /**
-        * Load the interwiki, trying first memcached then the DB
-        *
-        * @param string $prefix The interwiki prefix
-        * @return Interwiki|bool Interwiki if $prefix is valid, otherwise false
-        */
-       protected static function load( $prefix ) {
-               global $wgInterwikiExpiry;
-
-               $iwData = [];
-               if ( !Hooks::run( 'InterwikiLoadPrefix', [ $prefix, &$iwData ] ) ) {
-                       return Interwiki::loadFromArray( $iwData );
-               }
-
-               if ( is_array( $iwData ) ) {
-                       $iw = Interwiki::loadFromArray( $iwData );
-                       if ( $iw ) {
-                               return $iw; // handled by hook
-                       }
-               }
-
-               $iwData = ObjectCache::getMainWANInstance()->getWithSetCallback(
-                       wfMemcKey( 'interwiki', $prefix ),
-                       $wgInterwikiExpiry,
-                       function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
-                               $dbr = wfGetDB( DB_SLAVE );
-
-                               $setOpts += Database::getCacheSetOptions( $dbr );
-
-                               $row = $dbr->selectRow(
-                                       'interwiki',
-                                       Interwiki::selectFields(),
-                                       [ 'iw_prefix' => $prefix ],
-                                       __METHOD__
-                               );
-
-                               return $row ? (array)$row : '!NONEXISTENT';
-                       }
-               );
-
-               if ( is_array( $iwData ) ) {
-                       return Interwiki::loadFromArray( $iwData ) ?: false;
-               }
-
-               return false;
-       }
-
-       /**
-        * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc)
-        *
-        * @param array $mc Associative array: row from the interwiki table
-        * @return Interwiki|bool Interwiki object or false if $mc['iw_url'] is not set
-        */
-       protected static function loadFromArray( $mc ) {
-               if ( isset( $mc['iw_url'] ) ) {
-                       $iw = new Interwiki();
-                       $iw->mURL = $mc['iw_url'];
-                       $iw->mLocal = isset( $mc['iw_local'] ) ? (bool)$mc['iw_local'] : false;
-                       $iw->mTrans = isset( $mc['iw_trans'] ) ? (bool)$mc['iw_trans'] : false;
-                       $iw->mAPI = isset( $mc['iw_api'] ) ? $mc['iw_api'] : '';
-                       $iw->mWikiID = isset( $mc['iw_wikiid'] ) ? $mc['iw_wikiid'] : '';
-
-                       return $iw;
-               }
-
-               return false;
-       }
-
-       /**
-        * Fetch all interwiki prefixes from interwiki cache
-        *
-        * @param null|string $local If not null, limits output to local/non-local interwikis
-        * @return array List of prefixes
-        * @since 1.19
-        */
-       protected static function getAllPrefixesCached( $local ) {
-               global $wgInterwikiScopes, $wgInterwikiFallbackSite;
-               static $site;
-
-               wfDebug( __METHOD__ . "()\n" );
-               $data = [];
-               try {
-                       /* Resolve site name */
-                       if ( $wgInterwikiScopes >= 3 && !$site ) {
-                               $site = self::getCacheValue( '__sites:' . wfWikiID() );
-
-                               if ( $site == '' ) {
-                                       $site = $wgInterwikiFallbackSite;
-                               }
-                       }
-
-                       // List of interwiki sources
-                       $sources = [];
-                       // Global Level
-                       if ( $wgInterwikiScopes >= 2 ) {
-                               $sources[] = '__global';
-                       }
-                       // Site level
-                       if ( $wgInterwikiScopes >= 3 ) {
-                               $sources[] = '_' . $site;
-                       }
-                       $sources[] = wfWikiID();
-
-                       foreach ( $sources as $source ) {
-                               $list = self::getCacheValue( '__list:' . $source );
-                               foreach ( explode( ' ', $list ) as $iw_prefix ) {
-                                       $row = self::getCacheValue( "{$source}:{$iw_prefix}" );
-                                       if ( !$row ) {
-                                               continue;
-                                       }
-
-                                       list( $iw_local, $iw_url ) = explode( ' ', $row );
-
-                                       if ( $local !== null && $local != $iw_local ) {
-                                               continue;
-                                       }
-
-                                       $data[$iw_prefix] = [
-                                               'iw_prefix' => $iw_prefix,
-                                               'iw_url' => $iw_url,
-                                               'iw_local' => $iw_local,
-                                       ];
-                               }
-                       }
-               } catch ( CdbException $e ) {
-                       wfDebug( __METHOD__ . ": CdbException caught, error message was "
-                               . $e->getMessage() );
-               }
-
-               ksort( $data );
-
-               return array_values( $data );
-       }
-
-       /**
-        * Fetch all interwiki prefixes from DB
-        *
-        * @param string|null $local If not null, limits output to local/non-local interwikis
-        * @return array List of prefixes
-        * @since 1.19
-        */
-       protected static function getAllPrefixesDB( $local ) {
-               $db = wfGetDB( DB_SLAVE );
-
-               $where = [];
-
-               if ( $local !== null ) {
-                       if ( $local == 1 ) {
-                               $where['iw_local'] = 1;
-                       } elseif ( $local == 0 ) {
-                               $where['iw_local'] = 0;
-                       }
-               }
-
-               $res = $db->select( 'interwiki',
-                       self::selectFields(),
-                       $where, __METHOD__, [ 'ORDER BY' => 'iw_prefix' ]
-               );
-
-               $retval = [];
-               foreach ( $res as $row ) {
-                       $retval[] = (array)$row;
-               }
-
-               return $retval;
+               return MediaWikiServices::getInstance()->getInterwikiLookup()->invalidateCache( $prefix );
        }
 
        /**
         * Returns all interwiki prefixes
         *
+        * @deprecated since 1.28, unused. Use InterwikiLookup instead.
+        *
         * @param string|null $local If set, limits output to local/non-local interwikis
         * @return array List of prefixes
         * @since 1.19
         */
        public static function getAllPrefixes( $local = null ) {
-               global $wgInterwikiCache;
-
-               if ( $wgInterwikiCache ) {
-                       return self::getAllPrefixesCached( $local );
-               }
-
-               return self::getAllPrefixesDB( $local );
+               return MediaWikiServices::getInstance()->getInterwikiLookup()->getAllPrefixes( $local );
        }
 
        /**
@@ -476,19 +182,4 @@ class Interwiki {
                return !$msg->exists() ? '' : $msg->text();
        }
 
-       /**
-        * Return the list of interwiki fields that should be selected to create
-        * a new Interwiki object.
-        * @return string[]
-        */
-       public static function selectFields() {
-               return [
-                       'iw_prefix',
-                       'iw_url',
-                       'iw_api',
-                       'iw_wikiid',
-                       'iw_local',
-                       'iw_trans'
-               ];
-       }
 }
diff --git a/includes/interwiki/InterwikiLookup.php b/includes/interwiki/InterwikiLookup.php
new file mode 100644 (file)
index 0000000..459910a
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+namespace MediaWiki\Interwiki;
+
+/**
+ * Service interface for looking up Interwiki records.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+use Interwiki;
+
+/**
+ * Service interface for looking up Interwiki records.
+ *
+ * @singe 1.28
+ */
+interface InterwikiLookup {
+
+       /**
+        * Check whether an interwiki prefix exists
+        *
+        * @param string $prefix Interwiki prefix to use
+        * @return bool Whether it exists
+        */
+       public function isValidInterwiki( $prefix );
+
+       /**
+        * Fetch an Interwiki object
+        *
+        * @param string $prefix Interwiki prefix to use
+        * @return Interwiki|null|bool
+        */
+       public function fetch( $prefix );
+
+       /**
+        * Returns all interwiki prefixes
+        *
+        * @param string|null $local If set, limits output to local/non-local interwikis
+        * @return string[] List of prefixes
+        */
+       public function getAllPrefixes( $local = null );
+
+       /**
+        * Purge the in-process and persistent object cache for an interwiki prefix
+        * @param string $prefix
+        */
+       public function invalidateCache( $prefix );
+
+}
index 10d9cb9..cefc5bc 100644 (file)
@@ -695,11 +695,6 @@ class SkinTemplate extends Skin {
 
                        // No need to show Talk and Contributions to anons if they can't contribute!
                        if ( User::groupHasPermission( '*', 'edit' ) ) {
-                               // Show the text "Not logged in"
-                               $personal_urls['anonuserpage'] = [
-                                       'text' => $this->msg( 'notloggedin' )->text()
-                               ];
-
                                // Because of caching, we can't link directly to the IP talk and
                                // contributions pages. Instead we use the special page shortcuts
                                // (which work correctly regardless of caching). This means we can't
index 9771d45..40e47b4 100644 (file)
        "whatlinkshere-prev": "{{PLURAL:$1|قاباقکی|قاباقکی $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|سونراکی|سونراکی $1}}",
        "whatlinkshere-links": "← باغلانتیلار",
-       "whatlinkshere-hideredirs": "یول‌لاندیرمالاری $1",
+       "whatlinkshere-hideredirs": "یول‌لاندیرمالاری گیزلت",
        "whatlinkshere-hidetrans": "علاوه‌لری $1",
        "whatlinkshere-hidelinks": "باغلانتیلاری $1",
        "whatlinkshere-hideimages": "فایل باغلانتیلارینی $1",
        "logentry-newusers-byemail": "$3 ایستیفاده‌چی حسابی، $1 ایله {{GENDER:$2|یارادیلیب}} و رمز، ایمیل ایله گؤندریلیب‌دیر",
        "logentry-newusers-autocreate": "$1 ایشلدن حسابی اوْتوماتیک {{GENDER:$2|یارادیلدی}}",
        "logentry-protect-protect": "$1 $3-ی/و  {{GENDER:$2|قوْرودو}} $4",
-       "logentry-rights-rights": "$1، $3-ین قروپ عوضولوگونو $4-دن $5-ه {{GENDER:$2|دَییشدیردی}}",
+       "logentry-rights-rights": "$1، $3-ین قروپ عۆضولوگونو $4-دن $5-ه {{GENDER:$2|دَییشدیردی}}",
        "logentry-rights-rights-legacy": "$1، $3-ین قروپ عوضولوگونو {{GENDER:$2|دَییشدیردی}}",
        "logentry-rights-autopromote": "$1-ین مقامی اوتوماتیک $4-دن $5-ه {{GENDER:$2|آرتیریلدی}}",
        "logentry-upload-upload": "$1 $3 را {{GENDER:$2|یوکلندیردی}}",
index 38de629..88e60dd 100644 (file)
        "log-action-filter-protect-modify": "Яңынан тейәү",
        "log-action-filter-protect-unprotect": "Һаҡты алып ташлау",
        "log-action-filter-upload-upload": "Яңы күсереү",
-       "log-action-filter-upload-overwrite": "Ҡабаттан тейәү"
+       "log-action-filter-upload-overwrite": "Ҡабаттан тейәү",
+       "authmanager-email-label": "Электрон почта адресы",
+       "authmanager-realname-label": "Ысын исемегеҙ",
+       "changecredentials-submit": "Үҙгәртергә",
+       "removecredentials-submit-cancel": "Кире алырға"
 }
index 7a39fa8..4cfb425 100644 (file)
        "userlogin-resetpassword-link": "Забылі пароль?",
        "userlogin-helplink2": "Дапамога з уваходам у сыстэму",
        "userlogin-loggedin": "Вы ўжо ўвайшлі як {{GENDER:$1|$1}}.\nДля ўваходу пад іншым удзельнікам скарыстайцеся формай унізе.",
+       "userlogin-reauth": "Вы мусіце ўвайсьці яшчэ раз, каб пацьвердзіць, што вы — гэта {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Стварыць іншы рахунак",
        "createacct-emailrequired": "Адрас электроннай пошты",
        "createacct-emailoptional": "Адрас электроннай пошты (неабавязкова)",
index a239396..9b82777 100644 (file)
        "whatlinkshere-prev": "{{PLURAL:$1|دیمئ|$1 دیمئ مورد}}",
        "whatlinkshere-next": "{{PLURAL:$1|پدئ|$1 پدئ مورد}}",
        "whatlinkshere-links": "→ لینک",
-       "whatlinkshere-hideredirs": "$1 تغییرمسیر",
-       "whatlinkshere-hidetrans": "$1 تراگنجانش‌هان",
-       "whatlinkshere-hidelinks": "$1 لینک",
+       "whatlinkshere-hideredirs": "تغیرمسیرانی چیهرداتین",
+       "whatlinkshere-hidetrans": "تراگنجانش‌هانی چیهر داتین",
+       "whatlinkshere-hidelinks": "لینکاني چیهر داتین",
        "whatlinkshere-hideimages": "$1 فایلی لینکان",
        "whatlinkshere-filters": "فیلتر ئان",
        "autoblockid": "#$1 ئی اوتو بلاک",
index 732d94b..4ae6c23 100644 (file)
        "password-change-forbidden": "Na této wiki nemůžete měnit hesla.",
        "externaldberror": "Buď nastala chyba externí autentizační databáze, nebo nemáte dovoleno měnit svůj externí účet.",
        "login": "Přihlaste se",
+       "login-security": "Ověřte svou identitu",
        "nav-login-createaccount": "Přihlášení / vytvoření účtu",
        "userlogin": "Přihlášení / vytvoření účtu",
        "userloginnocreate": "Přihlášení",
        "userlogin-resetpassword-link": "Zapomněli jste heslo?",
        "userlogin-helplink2": "Nápověda k přihlašování",
        "userlogin-loggedin": "Již jste {{GENDER:$1|přihlášen|přihlášena}} jako $1.\nPomocí formuláře níže se můžete přihlásit jako jiný uživatel.",
+       "userlogin-reauth": "Abyste {{GENDER:$1|prokázal|prokázala|prokázali}}, že jste $1, musíte se znovu přihlásit.",
        "userlogin-createanother": "Vytvořit jiný účet",
        "createacct-emailrequired": "E-mailová adresa",
        "createacct-emailoptional": "E-mailová adresa (nepovinné)",
        "nocookiesnew": "Uživatelský účet byl vytvořen, ale nejste přihlášeni. {{SITENAME}} používá cookies k přihlášení uživatelů. Vy máte cookies vypnuty. Prosím, zapněte je a poté se přihlaste svým novým uživatelským jménem a heslem.",
        "nocookieslogin": "{{SITENAME}} používá cookies k přihlášení uživatelů. Vy máte cookies vypnuty. Prosím zapněte je a zkuste znovu.",
        "nocookiesfornew": "Uživatelský účet nebyl založen, neboť jsme nebyli schopni potvrdit jeho původ.\nUjistěte se, že máte povoleny cookies, obnovte tuto stránku a zkuste to znovu.",
+       "createacct-loginerror": "Účet byl úspěšně vytvořen, ale nemohli jste být automaticky přihlášeni. Pokračujte prosím na [[Special:UserLogin|ruční přihlášení]].",
        "noname": "{{GENDER:|Nezadal|Nezadala|Nezadali}} jste platné uživatelské jméno.",
        "loginsuccesstitle": "Přihlášení bylo úspěšné",
        "loginsuccess": "<strong>Nyní jste na {{grammar:6sg|{{SITENAME}}}} {{GENDER:$1|přihlášen jako uživatel|přihlášena jako uživatelka}} „$1“.</strong>",
-       "nosuchuser": "Neexistuje uživatel se jménem „$1“. U uživatelských jmen se rozlišují malá/velká písmena. Zkontrolujte zápis, nebo si [[Special:CreateAccount|vytvořte nový účet]].",
+       "nosuchuser": "Neexistuje uživatel se jménem „$1“.\nU uživatelských jmen se rozlišují malá/velká písmena.\nZkontrolujte zápis, nebo si [[Special:CreateAccount|vytvořte nový účet]].",
        "nosuchusershort": "Neexistuje uživatel se jménem „$1“. Zkontrolujte zápis.",
        "nouserspecified": "Musíte zadat uživatelské jméno.",
        "login-userblocked": "{{GENDER:$1|Tento uživatel je zablokován|Tato uživatelka je zablokována}}. Přihlášení není dovoleno.",
        "createacct-another-realname-tip": "Skutečné jméno je nepovinné.\nPokud se ho rozhodnete uvést, bude použito pro označení autorství vaší práce.",
        "pt-login": "Přihlášení",
        "pt-login-button": "Přihlásit se",
+       "pt-login-continue-button": "Pokračovat v přihlášení",
        "pt-createaccount": "Vytvoření účtu",
        "pt-userlogout": "Odhlásit se",
        "php-mail-error-unknown": "Neznámá chyba v PHP funkci mail()",
        "botpasswords-invalid-name": "Uvedené uživatelské jméno neobsahuje oddělovač hesel pro boty („$1“).",
        "botpasswords-not-exist": "Uživatel „$1“ nemá heslo pro bota nazvaného „$2“.",
        "resetpass_forbidden": "Hesla nelze změnit.",
+       "resetpass_forbidden-reason": "Hesla nelze změnit: $1",
        "resetpass-no-info": "K této stránce mají přímý přístup jen přihlášení uživatelé.",
        "resetpass-submit-loggedin": "Změnit heslo",
        "resetpass-submit-cancel": "Storno",
index f3d076b..91580b4 100644 (file)
        "password-change-forbidden": "Du kannst auf diesem Wiki keine Passwörter ändern.",
        "externaldberror": "Entweder liegt ein Fehler bei der externen Authentifizierung vor oder du darfst dein externes Benutzerkonto nicht aktualisieren.",
        "login": "Anmelden",
+       "login-security": "Verifiziere deine Identität",
        "nav-login-createaccount": "Anmelden / Benutzerkonto erstellen",
        "userlogin": "Anmelden / Benutzerkonto anlegen",
        "userloginnocreate": "Anmelden",
        "userlogin-resetpassword-link": "Passwort vergessen?",
        "userlogin-helplink2": "Hilfe beim Anmelden",
        "userlogin-loggedin": "Du bist bereits als {{GENDER:$1|$1}} angemeldet.\nBenutze das unten stehende Formular, um dich unter einem anderen Benutzernamen anzumelden.",
+       "userlogin-reauth": "Du musst dich erneut anmelden, um zu verifizieren, dass du {{GENDER:$1|$1}} bist.",
        "userlogin-createanother": "Ein weiteres Benutzerkonto erstellen",
        "createacct-emailrequired": "E-Mail-Adresse",
        "createacct-emailoptional": "E-Mail-Adresse (optional)",
        "createacct-email-ph": "Gib deine E-Mail-Adresse ein",
        "createacct-another-email-ph": "E-Mail-Adresse",
        "createaccountmail": "Ein temporäres Zufallspasswort verwenden und an die angegebene E-Mail-Adresse versenden",
+       "createaccountmail-help": "Kann verwendet werden, um für eine andere Person ein Benutzerkonto zu erstellen, ohne das Passwort zu erfahren.",
        "createacct-realname": "Bürgerlicher Name (optional)",
        "createaccountreason": "Grund:",
        "createacct-reason": "Begründung",
        "createacct-reason-ph": "Warum erstellst du ein anderes Benutzerkonto?",
+       "createacct-reason-help": "Im Neuanmeldungs-Logbuch angezeigte Nachricht",
        "createacct-submit": "Benutzerkonto erstellen",
        "createacct-another-submit": "Benutzerkonto erstellen",
+       "createacct-continue-submit": "Benutzerkontenerstellung fortfahren",
+       "createacct-another-continue-submit": "Benutzerkontenerstellung fortfahren",
        "createacct-benefit-heading": "{{SITENAME}} wird von Menschen wie dir geschaffen.",
        "createacct-benefit-body1": "{{PLURAL:$1|Bearbeitung|Bearbeitungen}}",
        "createacct-benefit-body2": "{{PLURAL:$1|Seite|Seiten}}",
        "nocookiesnew": "Der Benutzerzugang wurde erstellt, aber du bist nicht angemeldet. {{SITENAME}} benötigt für diese Funktion Cookies, bitte aktiviere diese und melde dich dann mit deinem neuen Benutzernamen und dem zugehörigen Passwort an.",
        "nocookieslogin": "{{SITENAME}} benutzt Cookies zur Anmeldung der Benutzer. Du hast Cookies deaktiviert, bitte aktiviere diese und versuche es erneut.",
        "nocookiesfornew": "Das Benutzerkonto wurde nicht erstellt, da die Datenherkunft nicht ermittelt werden konnte.\nBitte stelle sicher, dass du Cookies aktiviert hast. Lade diese Seite danach erneut und versuche es noch einmal.",
+       "createacct-loginerror": "Das Benutzerkonto wurde erfolgreich erstellt, aber du konntest nicht automatisch angemeldet werden. Bitte fahre mit der [[Special:UserLogin|manuellen Anmeldung]] fort.",
        "noname": "Du musst einen gültigen Benutzernamen angeben.",
        "loginsuccesstitle": "Angemeldet",
        "loginsuccess": "<strong>Du bist jetzt als „$1“ bei {{SITENAME}} angemeldet.</strong>",
        "createacct-another-realname-tip": "Der bürgerliche Name ist optional.\nWenn du ihn angibst, wird er für die Zuordnung der Beiträge verwendet.",
        "pt-login": "Anmelden",
        "pt-login-button": "Anmelden",
+       "pt-login-continue-button": "Anmeldung fortfahren",
        "pt-createaccount": "Benutzerkonto erstellen",
        "pt-userlogout": "Abmelden",
        "php-mail-error-unknown": "Unbekannter Fehler in der PHP-Funktion mail().",
        "botpasswords-invalid-name": "Der angegebene Benutzername enthält keinen Botpassworttrenner („$1“).",
        "botpasswords-not-exist": "Der Benutzer „$1“ hat kein Botpasswort mit dem Namen „$2“.",
        "resetpass_forbidden": "Das Passwort kann nicht geändert werden.",
+       "resetpass_forbidden-reason": "Die Passwörter können nicht geändert werden: $1",
        "resetpass-no-info": "Du musst dich anmelden, um auf diese Seite direkt zuzugreifen.",
        "resetpass-submit-loggedin": "Passwort ändern",
        "resetpass-submit-cancel": "Abbrechen",
        "passwordreset-emailsentusername": "Falls es eine E-Mail-Adresse gibt, die mit diesem Benutzernamen verknüpft ist, wird eine Passwort-Zurücksetzungs-E-Mail versandt.",
        "passwordreset-emailsent-capture": "Eine Passwortzurücksetzungs-E-Mail wurde versandt, die unten angezeigt wird.",
        "passwordreset-emailerror-capture": "Die unten angezeigte Passwortzurücksetzungs-E-Mail wurde generiert, allerdings ist der Versand an {{GENDER:$2|den Benutzer|die Benutzerin}} gescheitert: $1",
+       "passwordreset-emailsent-capture2": "Die Passwort-Zurücksetzungs-{{PLURAL:$1|E-Mail wurde|E-Mails wurden}} versandt. {{PLURAL:$1|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird unten angezeigt.",
+       "passwordreset-emailerror-capture2": "Das Senden der E-Mail an {{GENDER:$2|den Benutzer|die Benutzerin}} ist fehlgeschlagen: $1 {{PLURAL:$3|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird unten angezeigt.",
+       "passwordreset-nocaller": "Es muss ein Rufer angegeben werden",
+       "passwordreset-nosuchcaller": "Rufer ist nicht vorhanden: $1",
+       "passwordreset-ignored": "Die Passwortzurücksetzung konnte nicht verarbeitet werden. Vielleicht wurde kein Dienstanbieter konfiguriert?",
+       "passwordreset-invalideamil": "Ungültige E-Mail-Adresse",
+       "passwordreset-nodata": "Weder ein Benutzername noch eine E-Mail-Adresse wurde angegeben",
        "changeemail": "E-Mail-Adresse ändern oder entfernen",
        "changeemail-header": "Fülle dieses Formular vollständig aus, um deine E-Mail-Adresse zu ändern. Falls du die Zuweisung einer E-Mail-Adresse zu deinem Benutzerkonto entfernen möchtest, lasse beim Übermitteln des Formulars das Feld für die neue E-Mail-Adresse leer.",
        "changeemail-passwordrequired": "Du musst dein Passwort eingeben, um diese Änderung zu bestätigen.",
        "accmailtext": "Ein zufällig generiertes Passwort für [[User talk:$1|$1]] wurde an $2 versandt. Es kann auf der Seite ''[[Special:ChangePassword|Passwort ändern]]'' nach der Anmeldung geändert werden.",
        "newarticle": "(Neu)",
        "newarticletext": "Du bist einem Link zu einer Seite gefolgt, die nicht vorhanden ist.\nUm diese Seite anzulegen, trage deinen Text in das untenstehende Bearbeitungsfeld ein (weitere Informationen auf der [$1 Hilfeseite]).\nSofern du fälschlicherweise hier bist, klicke auf die Schaltfläche '''Zurück''' deines Browsers.",
-       "anontalkpagetext": "----''Diese Seite dient dazu, einem nicht angemeldeten Benutzer Nachrichten zu hinterlassen. Es wird seine IP-Adresse zur Identifizierung verwendet. IP-Adressen können von mehreren Benutzern gemeinsam verwendet werden. Wenn du mit den Kommentaren auf dieser Seite nichts anfangen kannst, richten sie sich vermutlich an einen früheren Inhaber deiner IP-Adresse und du kannst sie ignorieren. Du kannst dir auch ein [[Special:CreateAccount|Benutzerkonto erstellen]] oder dich [[Special:UserLogin|anmelden]], um künftig Verwechslungen mit anderen anonymen Benutzern zu vermeiden.''",
+       "anontalkpagetext": "----\n<em>Diese Seite dient dazu, einem nicht angemeldeten Benutzer Nachrichten zu hinterlassen.</em>\nEs wird seine IP-Adresse zur Identifizierung verwendet.\nIP-Adressen können von mehreren Benutzern gemeinsam verwendet werden.\nWenn du mit den Kommentaren auf dieser Seite nichts anfangen kannst, richten sie sich vermutlich an einen früheren Inhaber deiner IP-Adresse und du kannst sie ignorieren.\nDu kannst dir auch ein [[Special:CreateAccount|Benutzerkonto erstellen]] oder dich [[Special:UserLogin|anmelden]], um künftig Verwechslungen mit anderen anonymen Benutzern zu vermeiden.",
        "noarticletext": "Diese Seite enthält momentan noch keinen Text.\nDu kannst sie <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} erstellen]</span>,\nihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]]\noder die zugehörigen <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten]</span>.",
        "noarticletext-nopermission": "Diese Seite enthält momentan noch keinen Text und du bist auch nicht dazu berechtigt, diese Seite zu erstellen.\nDu kannst ihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]] oder die zugehörigen <span class=\"plainlinks\">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten].</span>",
        "missing-revision": "Die Version $1 der Seite namens „{{FULLPAGENAME}}“ ist nicht vorhanden.\n\nDieser Fehler wird normalerweise von einem veralteten Link zur Versionsgeschichte einer Seite verursacht, die zwischenzeitlich gelöscht wurde.\nEinzelheiten sind im [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Lösch-Logbuch] einsehbar.",
        "log-action-filter-suppress-block": "Benutzerunterdrückung durch Sperre",
        "log-action-filter-suppress-reblock": "Benutzerunterdrückung durch Neusperre",
        "log-action-filter-upload-upload": "Neue Hochladung",
-       "log-action-filter-upload-overwrite": "Wiederhochladung"
+       "log-action-filter-upload-overwrite": "Wiederhochladung",
+       "authmanager-authn-not-in-progress": "Die Authentifizierung ist nicht im Gang oder es sind Sitzungsdaten verloren gegangen. Bitte beginne von vorn.",
+       "authmanager-authn-no-primary": "Die angegebenen Anmeldeinformationen konnten nicht überprüft werden.",
+       "authmanager-authn-no-local-user": "Die angegebenen Anmeldeinformationen sind mit keinem Benutzer auf diesem Wiki verknüpft.",
+       "authmanager-authn-no-local-user-link": "Die angegebenen Anmeldeinformationen sind gültig, aber sind mit keinem Benutzer auf diesem Wiki verknüpft. Melde dich auf andere Weise an oder erstelle ein neues Benutzerkonto und du wirst die Möglichkeit haben, deine früheren Anmeldeinformationen mit diesem Konto zu verknüpfen.",
+       "authmanager-authn-autocreate-failed": "Die automatische Erstellung des lokalen Benutzerkontos ist fehlgeschlagen: $1",
+       "authmanager-change-not-supported": "Die angegebenen Anmeldeinformationen konnten nicht geändert werden, da sie von nichts genutzt werden würden.",
+       "authmanager-create-disabled": "Die Benutzerkontenerstellung ist deaktiviert.",
+       "authmanager-create-from-login": "Um dein Benutzerkonto zu erstellen, fülle bitte die unten stehenden Felder aus.",
+       "authmanager-create-not-in-progress": "Die Benutzerkontenerstellung ist nicht im Gang oder es sind Sitzungsdaten verloren gegangen. Bitte beginne von vorn.",
+       "authmanager-create-no-primary": "Die angegebenen Anmeldeinformationen konnten nicht für die Benutzerkontenerstellung verwendet werden.",
+       "authmanager-link-no-primary": "Die angegebenen Anmeldeinformationen konnten nicht für die Benutzerkontenverknüpfung verwendet werden.",
+       "authmanager-link-not-in-progress": "Die Benutzerkontenverknüpfung ist nicht im Gang oder es sind Sitzungsdaten verloren gegangen. Bitte beginne von vorn.",
+       "authmanager-authplugin-setpass-failed-title": "Passwortänderung fehlgeschlagen",
+       "authmanager-authplugin-setpass-failed-message": "Das Authentifizierungs-Plugin hat die Passwortänderung abgelehnt.",
+       "authmanager-authplugin-create-fail": "Das Authentifizierungs-Plugin hat die Benutzerkontenerstellung abgelehnt.",
+       "authmanager-authplugin-setpass-denied": "Das Authentifizierungs-Plugin erlaubt keine Passwortänderungen.",
+       "authmanager-authplugin-setpass-bad-domain": "Ungültige Domain.",
+       "authmanager-autocreate-noperm": "Die automatische Benutzerkontenerstellung ist nicht erlaubt.",
+       "authmanager-autocreate-exception": "Die automatische Benutzerkontenerstellung ist aufgrund früherer Fehler vorübergehend deaktiviert.",
+       "authmanager-userdoesnotexist": "Das Benutzerkonto „$1“ ist nicht registriert.",
+       "authmanager-userlogin-remembermypassword-help": "Ob das Passwort länger als die Sitzungslänge behalten werden soll.",
+       "authmanager-username-help": "Benutzername für die Authentifizierung.",
+       "authmanager-password-help": "Passwort für die Authentifizierung.",
+       "authmanager-domain-help": "Domain für die externe Authentifizierung.",
+       "authmanager-retype-help": "Passwort erneut zur Bestätigung eingeben.",
+       "authmanager-email-label": "E-Mail",
+       "authmanager-email-help": "E-Mail-Adresse",
+       "authmanager-realname-label": "Bürgerlicher Name",
+       "authmanager-realname-help": "Bürgerlicher Name des Benutzers",
+       "authmanager-provider-password": "Passwortbasierte Authentifizierung",
+       "authmanager-provider-password-domain": "Passwort- und domainbasierte Authentifizierung",
+       "authmanager-provider-temporarypassword": "Temporäres Passwort",
+       "authprovider-confirmlink-message": "Basierend auf deinen letzten Anmeldeversuchen können die folgenden Benutzerkonten mit deinem Wiki-Benutzerkonto verknüpft werden. Das Verknüpfen ermöglicht die Anmeldung über diese Konten. Bitte wähle das Benutzerkonto aus, das verknüpft werden soll.",
+       "authprovider-confirmlink-request-label": "Benutzerkonten, die verknüpft werden sollen",
+       "authprovider-confirmlink-success-line": "$1: Erfolgreich verknüpft.",
+       "authprovider-confirmlink-failed": "Die Benutzerkontenverknüpfung war nicht vollständig erfolgreich: $1",
+       "authprovider-confirmlink-ok-help": "Nach der Anzeige von Verknüpfungsfehlermeldungen fortfahren.",
+       "authprovider-resetpass-skip-label": "Überspringen",
+       "authprovider-resetpass-skip-help": "Das Zurücksetzen des Passworts überspringen.",
+       "authform-nosession-login": "Die Authentifizierung war erfolgreich, aber dein Browser kann sich deine Anmeldung nicht „merken“.\n\n$1",
+       "authform-nosession-signup": "Das Benutzerkonto wurde erstellt, aber dein Browser kann sich deine Anmeldung nicht „merken“.\n\n$1",
+       "authform-newtoken": "Fehlender Token. $1",
+       "authform-notoken": "Fehlender Token",
+       "authform-wrongtoken": "Falscher Token",
+       "specialpage-securitylevel-not-allowed-title": "Nicht erlaubt",
+       "specialpage-securitylevel-not-allowed": "Leider bist du nicht berechtigt, diese Seite zu benutzen, da deine Identität nicht verifiziert werden konnte.",
+       "authpage-cannot-login": "Anmeldung konnte nicht gestartet werden.",
+       "authpage-cannot-login-continue": "Anmeldung konnte nicht fortgeführt werden. Vielleicht liegt bei deiner Sitzung eine Zeitüberschreitung vor.",
+       "authpage-cannot-create": "Benutzerkontenerstellung konnte nicht gestartet werden.",
+       "authpage-cannot-create-continue": "Die Benutzerkontenerstellung konnte nicht fortgeführt werden. Vielleicht liegt bei deiner Sitzung eine Zeitüberschreitung vor.",
+       "authpage-cannot-link": "Die Benutzerkontenverknüpfung konnte nicht gestartet werden.",
+       "authpage-cannot-link-continue": "Die Benutzerkontenverknüpfung konnte nicht fortgeführt werden. Vielleicht liegt bei deiner Sitzung eine Zeitüberschreitung vor.",
+       "cannotauth-not-allowed-title": "Zugriff verweigert",
+       "cannotauth-not-allowed": "Du bist nicht berechtigt, diese Seite zu verwenden.",
+       "changecredentials": "Anmeldeinformationen ändern",
+       "changecredentials-submit": "Ändern",
+       "changecredentials-submit-cancel": "Abbrechen",
+       "changecredentials-invalidsubpage": "$1 ist kein gültiger Typ für Anmeldeinformationen.",
+       "changecredentials-success": "Deine Anmeldeinformationen wurden geändert.",
+       "removecredentials": "Anmeldeinformationen entfernen",
+       "removecredentials-submit": "Entfernen",
+       "removecredentials-submit-cancel": "Abbrechen",
+       "removecredentials-invalidsubpage": "$1 ist kein gültiger Typ für Anmeldeinformationen.",
+       "removecredentials-success": "Deine Anmeldeinformationen wurden entfernt.",
+       "credentialsform-provider": "Typ der Anmeldeinformationen:",
+       "credentialsform-account": "Benutzerkontenname:",
+       "cannotlink-no-provider-title": "Es gibt keine verknüpfbaren Benutzerkonten",
+       "cannotlink-no-provider": "Es gibt keine verknüpfbaren Benutzerkonten.",
+       "linkaccounts": "Benutzerkonten verknüpfen",
+       "linkaccounts-success-text": "Das Benutzerkonto wurde verknüpft.",
+       "linkaccounts-submit": "Benutzerkonten verknüpfen",
+       "unlinkaccounts": "Benutzerkonten trennen",
+       "unlinkaccounts-success": "Das Benutzerkonto wurde getrennt."
 }
index 88c2891..9ff372c 100644 (file)
        "noname": "Yew nameyo maqbul bınuse.",
        "loginsuccesstitle": "Hesab abıya",
        "loginsuccess": "'''{{SITENAME}} dı name dê \"$1\" şıma hesab akerdo.'''",
-       "nosuchuser": "Ebe namey \"$1\"i yew karber çıniyo.\nNuştışê namanê karberan de herfa pil u qıce rê diqet kerên.\nNuştışê xo qonrol kerên, ya zi [[Special:CreateAccount|yew hesabo newe akerên]].",
+       "nosuchuser": "\"$1\" ya yew namey karberi çıniyo.\nNuştışê namanê karberan de herfa pil u qıce rê diqet kerên.\nNuştışê xo qontrol kerên, ya zi [[Special:CreateAccount|yew hesabo newe akerê]].",
        "nosuchusershort": "No \"$1\" name de yew ten çino. Kontrolê nuştışi bıkere.",
        "nouserspecified": "Şıma gani yew name bıde.",
        "login-userblocked": "No karber/na karbere blokekerdeyo/blokekerdiya. Cıkewtışi rê musade çıniyo.",
index 051f1a8..269afc3 100644 (file)
        "versionrequiredtext": "ये पाना प्रयोग गर्नका लागि MediaWiki $1 संस्करण चाहिन्छ ।\nहेर  [[Special:Version|version page]]",
        "ok": "भयो",
        "retrievedfrom": " \"$1\" बठे निकालिया",
-       "youhavenewmessages": "तमखी लेखा($2)मी $1 छ।",
+       "youhavenewmessages": "तमखी लेखा($2)मी $1 छ ।",
        "youhavenewmessagesfromusers": "तमखी लेखा {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्तान}}($2)बठे$1",
        "youhavenewmessagesmanyusers": "तमलाई धेरै प्रयोगकर्ताहरू($2) बठे $1 छ ।",
        "newmessageslinkplural": "{{PLURAL:$1|एक नौलो रैबार|999=नौला रैबारहरू}}",
        "viewsource-title": " $1 को स्रोत हेर",
        "actionthrottled": "कार्य रोकिईयो",
        "actionthrottledtext": "स्पामको रोकथामको लागि , तमीलाई यो कार्य नापै समयमी मैथै पटक गद्दाबठे सिमित गरियाको छ, र तमीले आफ्नो सिमा पार गरिसक्याछौ ।\nकृपया केही मिनेट पछि पुन: प्रयास गर  ।",
+       "viewsourcetext": "तम ये पृष्ठको स्रोत हेद्दु सकुन्छौ और उईको नक्कल उताद्दु सकुन्छौ |",
        "viewyourtext": "यै पानामी रह्याका '''तमरा सम्पादनहरू''' हेद्द या प्रतिलिपी गद्द सक्द्या हौ :",
        "editinginterface": "<strong>चेतावनी:</strong> तमी यै पानालाई सम्पादन गद्द लाग्याछौ, जनले सफ्टवेयरको लागि \nइन्टरफेस सामग्रीहरू प्रदान गरन्छ।\nयै पानामी गरियाको परिवर्तनले यै विकिमी अरु प्रयोगकर्तानको इन्टरफेसको प्रदर्शनमी प्रभाव पडन्छ ।",
        "namespaceprotected": "तमलाई '''$1'''  नेमस्पेसमी रह्याका पानाहरू सम्पादन गद्या अनुमति छैन ।",
        "resetpass_submit": "पासवर्ड व्यवस्थित गरी र प्रवेशगर्ने",
        "changepassword-success": "तमरो पासवर्ड सफलतापूर्वक परिवर्तन भयो!",
        "changepassword-throttled": "तमले अलै भौत फेर प्रवेशका निम्ति प्रयास गरया छौ।\nकृपया $1 थोक्कै जागी मात्र प्रयास गर।",
+       "botpasswords-label-grants-column": "प्रदान भयो",
+       "botpasswords-created-title": "बोट को पासवर्ड बन्यो",
+       "botpasswords-deleted-title": "बोट को पासवर्ड मेटियो",
        "resetpass_forbidden": "पासवर्ड परिवर्तन गर्न नाइँमिल्लो",
+       "resetpass_forbidden-reason": "पासवर्ड परिवर्तन गद्दु नाइँमिल्लो:$1",
        "resetpass-no-info": "ये पाना सिधाई हेद्दाई तमले प्रवेश गद्दु पडून्छ ।",
        "resetpass-submit-loggedin": "पासवर्ड परिवर्तन गर",
        "resetpass-submit-cancel": "रद्द",
index 7277348..41c9e92 100644 (file)
        "password-change-forbidden": "Ve ne povas ŝanĝi pasvortojn en ĉi tiu vikio.",
        "externaldberror": "Aŭ estis datenbaza eraro rilate al ekstera aŭtentikigado, aŭ vi ne rajtas ĝisdatigi vian eksteran konton.",
        "login": "Ensaluti",
+       "login-security": "Kontrolu vian identecon",
        "nav-login-createaccount": "Ensaluti / Krei novan konton",
        "userlogin": "Ensaluti / Krei novan konton",
        "userloginnocreate": "Ensaluti",
        "userlogin-resetpassword-link": "Ĉu vi forgesis vian pasvorton?",
        "userlogin-helplink2": "Helpo pri ensaluto",
        "userlogin-loggedin": "Vi jam estas ensalutita kiel {{GENDER:$1|$1}}.\nUzu la formularon suben por ensaluti kiel alia uzanto.",
+       "userlogin-reauth": "Vi devas ensaluti denove por konfirmi ke vi estas {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Krei alian konton",
        "createacct-emailrequired": "Retpoŝta adreso",
        "createacct-emailoptional": "Retpoŝta adreso (nedeviga)",
        "createacct-email-ph": "Enigu vian retpoŝtan adreson",
        "createacct-another-email-ph": "Enigu la retpoŝtan adreson",
        "createaccountmail": "Uzi provizoran hazardsignan pasvorton kaj sendi ĝin al la retpoŝta adreso ĉi-suba",
+       "createaccountmail-help": "Uzebla por krei konton de alia persono sen lerni la pasvorton.",
        "createacct-realname": "Vera nomo (nedeviga)",
        "createaccountreason": "Kialo:",
        "createacct-reason": "Kialo",
        "createacct-reason-ph": "Kial vi kreas plian konton",
+       "createacct-reason-help": "Mesaĝo vidigita en la protokolo pri kreado de konto",
        "createacct-submit": "Krei konton",
        "createacct-another-submit": "Krei konton",
+       "createacct-continue-submit": "Daŭri kreadon de konto",
+       "createacct-another-continue-submit": "Daŭri kreadon de konto",
        "createacct-benefit-heading": "{{SITENAME}} estas kreata de homoj kiel vi.",
        "createacct-benefit-body1": "{{PLURAL:$1|redakto|redaktoj}}",
        "createacct-benefit-body2": "{{PLURAL:$1|paĝo|paĝoj}}",
        "createacct-another-realname-tip": "La vera nomo estas nenecesa.\nSe vi decidas indiki ĝin, ĝi estos uzata por montri atribuadon de viaj kontribuoj.",
        "pt-login": "Ensaluti",
        "pt-login-button": "Ensaluti",
+       "pt-login-continue-button": "Daŭri ensaluton",
        "pt-createaccount": "Krei konton",
        "pt-userlogout": "Elsaluti",
        "php-mail-error-unknown": "Nekonata eraro en la funkcio mail() de PHP",
        "botpasswords-invalid-name": "La difinita uzantnomo malenhavas la robotopasvortan disigilon (\"$1\").",
        "botpasswords-not-exist": "Uzanto \"$1\" ne havas robotopasvorton, kiu nomiĝas \"$2\".",
        "resetpass_forbidden": "Pasvortoj ne estas ŝanĝeblaj",
+       "resetpass_forbidden-reason": "Pasvortoj ne povas esti ŝanĝita: $1",
        "resetpass-no-info": "Vi devas ensaluti por atingi ĉi tiun paĝon rekte.",
        "resetpass-submit-loggedin": "Ŝanĝi pasvorton",
        "resetpass-submit-cancel": "Nuligi",
        "passwordreset-emailsentusername": "Se estas retpoŝta adreso, kiu estas asociita kun tiu uzantnomo, tiam ni sendos retpoŝtan mesaĝon pri reagordado de la pasvorto.",
        "passwordreset-emailsent-capture": "Retpoŝto kun renovigita pasvorto estis sendita, kiu estas montrata malsupre.",
        "passwordreset-emailerror-capture": "Retpoŝto kun renovigita pasvorto estis generita, montrata sube, sed sendado al la {{GENDER:$2|uzanto}} malsukcesis: $1",
+       "passwordreset-nocaller": "Vokanto devas esti provizita",
+       "passwordreset-nosuchcaller": "Vokanto ne ekzistas: $1",
+       "passwordreset-invalideamil": "Nevalida retpoŝta adreso",
        "changeemail": "Ŝanĝi aŭ forigi retpoŝtadreson",
        "changeemail-header": "Plenigu ĉi tiun formularon por ŝanĝi vian retpoŝtadreson. Se vi volas forigi la difinon de retpoŝtadreso por via uzantokonto, lasu la kampon por la nova retpoŝtadreso malplena ĉe la transigo.",
        "changeemail-passwordrequired": "Vi devas entajpi vian pasvorton, por konfirmi ĉi tiun ŝanĝon.",
index 416ed94..c254685 100644 (file)
                        "Lemondoge",
                        "Jdforrester",
                        "Yasten",
-                       "Psychoslave"
+                       "Psychoslave",
+                       "Trial"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "password-change-forbidden": "Vous ne pouvez pas modifier les mots de passe sur ce wiki.",
        "externaldberror": "Soit une erreur s’est produite sur la base de données d’authentification, soit vous n’êtes pas autorisé à mettre à jour votre compte externe.",
        "login": "Connexion",
+       "login-security": "Vérifier votre identité",
        "nav-login-createaccount": "Créer un compte ou se connecter",
        "userlogin": "Créer un compte ou se connecter",
        "userloginnocreate": "Connexion",
        "userlogin-resetpassword-link": "Mot de passe oublié ?",
        "userlogin-helplink2": "Aide pour se connecter",
        "userlogin-loggedin": "Vous êtes déjà connecté{{GENDER:$1||e|(e)}} en tant que $1.\nUtilisez le formulaire ci-dessous pour vous connecter avec un autre compte utilisateur.",
+       "userlogin-reauth": "Vous devez vous reconnecter pour vérifier que vous êtes {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Créer un autre compte",
        "createacct-emailrequired": "Adresse de courriel",
        "createacct-emailoptional": "Adresse de courriel (facultative)",
        "createacct-email-ph": "Entrez votre adresse de courriel",
        "createacct-another-email-ph": "Entrez l’adresse de courriel",
        "createaccountmail": "Utiliser un mot de passe aléatoire temporaire et l’envoyer à l’adresse de courriel spécifiée",
+       "createaccountmail-help": "Peut être utilisé pour créer un compte pour une autre personne sans connaître le mot de passe.",
        "createacct-realname": "Nom réel (facultatif)",
        "createaccountreason": "Motif :",
        "createacct-reason": "Motif",
        "createacct-reason-ph": "Pourquoi créez-vous un autre compte",
+       "createacct-reason-help": "Message affiché dans le journal de création de compte",
        "createacct-submit": "Créez votre compte",
        "createacct-another-submit": "Créer le compte",
+       "createacct-continue-submit": "Continuer la création de compte",
+       "createacct-another-continue-submit": "Continuer la création de compte",
        "createacct-benefit-heading": "{{SITENAME}} est écrit par des gens comme vous.",
        "createacct-benefit-body1": "modification{{PLURAL:$1||s}}",
        "createacct-benefit-body2": "page{{PLURAL:$1||s}}",
        "nocookiesnew": "Le compte utilisateur a été créé, mais vous n’êtes pas connecté{{GENDER:||e|(e)}}.\n{{SITENAME}} utilise des cookies pour conserver la connexion mais vous les avez désactivés.\nVeuillez les activer et vous reconnecter avec le même nom et le même mot de passe.",
        "nocookieslogin": "{{SITENAME}} utilise des cookies pour conserver la connexion mais vous les avez désactivés.\nVeuillez les activer et vous reconnecter.",
        "nocookiesfornew": "Le compte utilisateur n’a pas été créé, car nous n’avons pas pu identifier son origine.\nVérifiez que vous avez activé les cookies, rechargez la page et essayez à nouveau.",
+       "createacct-loginerror": "Le compte a bien été créé mais vous ne pouvez pas vous connecter automatiquement? Veuillez vous [[Special:UserLogin|connecter manuellement]].",
        "noname": "Vous n’avez pas saisi un nom d’utilisateur valide.",
        "loginsuccesstitle": "Connecté",
        "loginsuccess": "<strong>Vous êtes maintenant connecté{{GENDER:$1||e|(e)}} à {{SITENAME}} en tant que « $1 ».</strong>",
-       "nosuchuser": "L'utilisateur « $1 » n’existe pas.\nLes noms d’utilisateurs sont sensibles à la casse.\nVérifiez l’orthographe, ou [[Special:CreateAccount|créez un nouveau compte]].",
+       "nosuchuser": "L’utilisateur « $1 » n’existe pas.\nLes noms d’utilisateurs sont sensibles à la casse.\nVérifiez l’orthographe, ou [[Special:CreateAccount|créez un nouveau compte]].",
        "nosuchusershort": "Il n’y a pas de contributeur avec le nom « $1 ».\nVeuillez vérifier l’orthographe.",
        "nouserspecified": "Vous devez saisir un nom d’utilisateur.",
        "login-userblocked": "Cet utilisateur est bloqué. Connexion non autorisée.",
        "createacct-another-realname-tip": "Le véritable nom est optionnel.\nSi vous décidez de le fournir, il sera utilisé pour créditer l’auteur de ses travaux.",
        "pt-login": "Se connecter",
        "pt-login-button": "Se connecter",
+       "pt-login-continue-button": "Continuer la connexion",
        "pt-createaccount": "Créer un compte",
        "pt-userlogout": "Se déconnecter",
        "php-mail-error-unknown": "Erreur inconnue dans la fonction <code>mail()</code> de PHP.",
        "botpasswords-invalid-name": "Le nom d’utilisateur spécifié ne contient pas de séparateur de mot de passe de robots (« $1 »).",
        "botpasswords-not-exist": "L’utilisateur « $1 » n’a pas de nom de mot de passe de robots appelé « $2 ».",
        "resetpass_forbidden": "Les mots de passe ne peuvent pas être changés",
+       "resetpass_forbidden-reason": "Les mots de passe ne peuvent pas être modifiés : $1",
        "resetpass-no-info": "Vous devez être connecté pour avoir accès à cette page.",
        "resetpass-submit-loggedin": "Changer de mot de passe",
        "resetpass-submit-cancel": "Annuler",
        "passwordreset-emailsentusername": "S’il y a une adresse de courriel associée à ce nom d’utilisateur, alors un courriel de réinitialisation de mot de passe sera envoyé.",
        "passwordreset-emailsent-capture": "Un courriel de réinitialisation de mot de passe a été envoyé, qui est affiché ci-dessous.",
        "passwordreset-emailerror-capture": "Un courriel de réinitialisation de mot de passe a été généré, qui est affiché ci-dessous, mais l'envoi à l'{{GENDER:$2|utilisateur|utilisatrice}} a échoué : $1",
+       "passwordreset-emailsent-capture2": "The password reset {{PLURAL:$1|Le courriel de réinitialisation du mot de passe a été envoyé|Les courriels de réinitialisation du mot de passe ont été envoyés}}. {{PLURAL:$1|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et mots de passe est affichée}} ci-dessous.",
+       "passwordreset-emailerror-capture2": "L’envoi de courriel à {{GENDER:$2|l’utilisateur|l’utilisatrice}} a échoué : $1 {{PLURAL:$3|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et des mots de passe est affichée}} ci-dessous.",
+       "passwordreset-nocaller": "Un appelant doit être fourni",
+       "passwordreset-nosuchcaller": "L’appelant n’existe pas : $1",
+       "passwordreset-ignored": "La réinitialisation du mot de passe n’a pas été gérée. Peut-être aucun fournisseur n’a été configuré ?",
+       "passwordreset-invalideamil": "Adresse de messagerie non valide",
+       "passwordreset-nodata": "Aucun nom d’utilisateur ni d’adresse de messagerie n’a été fourni",
        "changeemail": "Changer ou supprimer l’adresse de courriel",
        "changeemail-header": "Complétez ce formulaire pour modifier votre adresse de courriel. Si vous voulez supprimer l’association d’une adresse de courriel avec votre compte, laissez la nouvelle adresse de courriel vide lors de la soumission du formulaire.",
        "changeemail-passwordrequired": "Vous devrez saisir votre mot de passe pour confirmer cette modification.",
        "accmailtext": "Un mot de passe généré aléatoirement pour [[User talk:$1|$1]] a été envoyé à $2.\nIl peut être modifié sur la page ''[[Special:ChangePassword|Changement de mot de passe]]'' après connexion.",
        "newarticle": "(Nouveau)",
        "newarticletext": "Vous avez suivi un lien vers une page qui n’existe pas encore. \nAfin de créer cette page, entrez votre texte dans la boîte ci-après (vous pouvez consulter [$1 la page d’aide] pour plus d’informations). \nSi vous êtes arrivé{{GENDER:||e}} ici par erreur, cliquez sur le bouton '''retour''' de votre navigateur.",
-       "anontalkpagetext": "---- ''Vous êtes sur la page de discussion d'un utilisateur anonyme qui n'a pas encore créé de compte ou qui n'en utilise pas. Pour cette raison, nous devons utiliser son adresse IP pour l'identifier. Une adresse IP peut être partagée par plusieurs utilisateurs. Si vous êtes un{{GENDER:||e|}} utilisat{{GENDER:|eur|rice|eur}} anonyme et si vous constatez que des commentaires qui ne vous concernent pas vous ont été adressés, vous pouvez [[Special:CreateAccount|créer un compte]] ou [[Special:UserLogin|vous connecter]] afin d'éviter toute confusion future avec d'autres contributeurs anonymes.''",
+       "anontalkpagetext": "----\n<em>Vous êtes sur la page de discussion d’un utilisateur anonyme qui n’a pas encore créé de compte ou qui n’en utilise pas</em>.\nPour cette raison, nous devons utiliser son adresse IP pour l’identifier.\nUne adresse IP peut être partagée par plusieurs utilisateurs.\nSi vous êtes un{{GENDER:||e|}} utilisat{{GENDER:|eur|rice|eur}} anonyme et si vous constatez que des commentaires qui ne vous concernent pas vous ont été adressés, vous pouvez [[Special:CreateAccount|créer un compte]] ou [[Special:UserLogin|vous connecter]] afin d’éviter toute confusion future avec d’autres contributeurs anonymes.",
        "noarticletext": "Il n’y a pour l’instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|lancer une recherche sur ce titre]] dans les autres pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les opérations liées]\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} créer cette page]</span>.",
        "noarticletext-nopermission": "Il n'y a pour l'instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|faire une recherche sur ce titre]] dans les autres pages,\nou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les journaux associés]</span>.",
        "missing-revision": "La révision n° $1 de la page intitulée « {{FULLPAGENAME}} » n'existe pas.\n\nCela survient en général en suivant un lien historique obsolète vers une page qui a été supprimée.\nVous pouvez trouver plus de détails dans le [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} journal des suppressions].",
        "timezone-local": "Local",
        "duplicate-defaultsort": "Attention : la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
        "duplicate-displaytitle": "<strong>Attention :</strong> Le titre d'affichage « $2 » remplace l'ancien titre d'affichage « $1 ».",
-       "restricted-displaytitle": "<strong>Avertissement :</strong> le titre d’affichae \"$1\" a été ignoré car il n'est pas équivalent au titre effectif de la page.",
+       "restricted-displaytitle": "<strong>Avertissement :</strong> le titre d’affichage \"$1\" a été ignoré car il n'est pas équivalent au titre effectif de la page.",
        "invalid-indicator-name": "<strong>Erreur :</strong> L’attribut <code>name</code> des indicateurs d’état de la page ne doit pas être vide.",
        "version": "Version",
        "version-extensions": "Extensions installées",
        "log-action-filter-suppress-block": "Suppression d’utilisateur par blocage",
        "log-action-filter-suppress-reblock": "Suppression d’utilisateur par blocage réitéré",
        "log-action-filter-upload-upload": "Nouveau téléversement",
-       "log-action-filter-upload-overwrite": "Réitérer le téléversement"
+       "log-action-filter-upload-overwrite": "Réitérer le téléversement",
+       "authmanager-authn-not-in-progress": "L’authentification n’est pas en cours ou les données de session ont été perdues. Veuillez recommencer depuis le début.",
+       "authmanager-authn-no-primary": "Les informations d’identification fournies n’ont pas pu être authentifiées.",
+       "authmanager-authn-no-local-user": "Les informations d’identification ne sont associées à aucun utilisateur sur ce wiki.",
+       "authmanager-authn-no-local-user-link": "Les informations d’authentification sont valides mais ne sont associées à aucun utilisateur sur ce wiki. Connectez-vous d’une autre manière, ou créez un nouvel utilisateur, et vous aurez la possibilité de lier vos informations précédentes à ce compte.",
+       "authmanager-authn-autocreate-failed": "La création automatique d’un compte local a échoué : $1",
+       "authmanager-change-not-supported": "Les informations d’identification fournies ne peuvent pas être modifiées, car rien ne les utiliserait.",
+       "authmanager-create-disabled": "La création de compte est désactivée.",
+       "authmanager-create-from-login": "Pour créer votre compte, veuillez remplir les champs ci-dessous.",
+       "authmanager-create-not-in-progress": "La création de compte n’est pas en cours, ou les données de session ont été perdues. Veuillez recommencer depuis le début.",
+       "authmanager-create-no-primary": "Les informations d’identification fournies n’ont pas pu être utilisées pour la création de compte.",
+       "authmanager-link-no-primary": "Les informations d’identification fournies n’ont pas pu être utilisées pour lier un compte.",
+       "authmanager-link-not-in-progress": "La liaison de compte n’est pas en cours ou les données de session ont été perdues. Veuillez redémarrer depuis le début.",
+       "authmanager-authplugin-setpass-failed-title": "Échec du changement de mot de passe",
+       "authmanager-authplugin-setpass-failed-message": "Le module d’authentification a refusé le changement de mot de passe.",
+       "authmanager-authplugin-create-fail": "Le module d’authentification a refusé la création de compte.",
+       "authmanager-authplugin-setpass-denied": "Le module d’authentification ne permet pas la modification des mots de passe.",
+       "authmanager-authplugin-setpass-bad-domain": "Domaine non valide.",
+       "authmanager-autocreate-noperm": "La création automatique de compte n’est pas autorisée.",
+       "authmanager-autocreate-exception": "La création automatique de compte est désactivée temporairement, du fait d’erreurs antérieures.",
+       "authmanager-userdoesnotexist": "Le compte utilisateur « $1 » n’est pas inscrit.",
+       "authmanager-userlogin-remembermypassword-help": "Indique si le mot de passe doit être mémorisé au-delà de la durée de la session.",
+       "authmanager-username-help": "Nom d’utilisateur pour l’authentification.",
+       "authmanager-password-help": "Mot de passe pour l’authentification.",
+       "authmanager-domain-help": "Domaine pour l’authentification externe.",
+       "authmanager-retype-help": "Mot de passe de nouveau pour confirmation.",
+       "authmanager-email-label": "Courriel",
+       "authmanager-email-help": "Adresse de messagerie",
+       "authmanager-realname-label": "Nom réel",
+       "authmanager-realname-help": "Nom réel de l’utilisateur",
+       "authmanager-provider-password": "Authentification par mot de passe",
+       "authmanager-provider-password-domain": "Authentification par mot de passe et domaine",
+       "authmanager-provider-temporarypassword": "Mot de passe temporaire",
+       "authprovider-confirmlink-message": "D’après vos dernières tentatives de connexion, les comptes suivants peuvent être liés à votre compte wiki. Les lier vous permettra de se connecter via ces comptes. Veuillez sélectionner lesquels doivent être liés.",
+       "authprovider-confirmlink-request-label": "Comptes qui doivent être liés",
+       "authprovider-confirmlink-success-line": "$1 : Liés avec succès.",
+       "authprovider-confirmlink-failed": "La liaison du compte n’a pas bien réussi : $1",
+       "authprovider-confirmlink-ok-help": "Continuer après l’affichage des messages d’échec de liaison.",
+       "authprovider-resetpass-skip-label": "Sauter",
+       "authprovider-resetpass-skip-help": "Sauter la réinitialisation du mot de passe.",
+       "authform-nosession-login": "L’authentification aréussi, mais votre navigateur ne peut pas se « souvenir » d’avoir été connecté.\n\n$1",
+       "authform-nosession-signup": "Le compte a été créé, mais votre navigateur ne peut pas se « souvenir » avoir été connecté.",
+       "authform-newtoken": "Jeton manquant. $1",
+       "authform-notoken": "Jeton manquant",
+       "authform-wrongtoken": "Mauvais jeton",
+       "specialpage-securitylevel-not-allowed-title": "Interdit",
+       "specialpage-securitylevel-not-allowed": "Désolé, vous n’êtes pas autorisé à utiliser cette page car votre identité n’a pu être vérifiée.",
+       "authpage-cannot-login": "Impossible de démarrer la connexion.",
+       "authpage-cannot-login-continue": "Impossible de continuer la connexion. Votre session a certainement expiré.",
+       "authpage-cannot-create": "Impossible de commencer la création de compte."
 }
index 9196f2d..428c8dd 100644 (file)
        "tog-showtoolbar": "הצגת סרגל העריכה",
        "tog-editondblclick": "עריכת דפים בלחיצה כפולה",
        "tog-editsectiononrightclick": "עריכת פסקאות באמצעות לחיצה ימנית על כותרות הפסקאות",
-       "tog-watchcreations": "×\94×\95ספת ×\93פ×\99×\9d ×©×\99צרת×\99 ×\95ק×\91צ×\99×\9d ×©×\94×¢×\9c×\99ת×\99 ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
-       "tog-watchdefault": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×¢×¨×\9bת×\99 ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
-       "tog-watchmoves": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×\94×¢×\91רת×\99 ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
-       "tog-watchdeletion": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×\9e×\97קת×\99 ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
-       "tog-watchuploads": "×\94×\95ספת ×§×\91צ×\99×\9d ×\97×\93ש×\99×\9d ×©×\94×¢×\9c×\99ת×\99 ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
-       "tog-watchrollback": "×\94×\95ספת ×\93פ×\99×\9d ×©×\91×\99צעת×\99 ×\91×\94×\9d ×©×\97×\96×\95ר ×\9e×\94×\99ר ×\9cרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\99",
+       "tog-watchcreations": "×\94×\95ספת ×\93פ×\99×\9d ×©×\90× ×\99 {{GENDER:|×\99×\95צר|×\99×\95צרת}} ×\95ק×\91צ×\99×\9d ×©×\90× ×\99 ×\9e×¢×\9c×\94 ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchdefault": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×\90× ×\99 {{GENDER:|×¢×\95ר×\9a|×¢×\95ר×\9bת}} ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchmoves": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×\90× ×\99 {{GENDER:|×\9e×¢×\91×\99ר|×\9e×¢×\91×\99ר×\94}} ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchdeletion": "×\94×\95ספת ×\93פ×\99×\9d ×\95ק×\91צ×\99×\9d ×©×\90× ×\99 {{GENDER:|×\9e×\95×\97ק|×\9e×\95×\97קת}} ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchuploads": "×\94×\95ספת ×§×\91צ×\99×\9d ×\97×\93ש×\99×\9d ×©×\90× ×\99 ×\9e×¢×\9c×\94 ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
+       "tog-watchrollback": "×\94×\95ספת ×\93פ×\99×\9d ×©×\90× ×\99 {{GENDER:|×\9e×\91צע|×\9e×\91צעת}} ×\91×\94×\9d ×©×\97×\96×\95ר ×\9e×\94×\99ר ×\9cרש×\99×\9eת ×\94×\9eעק×\91",
        "tog-minordefault": "סימון כל עריכה כמשנית כברירת מחדל",
        "tog-previewontop": "הצגת תצוגה מקדימה לפני תיבת העריכה",
        "tog-previewonfirst": "הצגת תצוגה מקדימה בעריכה הראשונה",
        "go": "הצגה",
        "searcharticle": "לדף",
        "history": "היסטוריית הדף",
-       "history_short": "×\94×\99ס×\98×\95ר×\99×\94",
+       "history_short": "×\92רס×\90×\95ת ×§×\95×\93×\9e×\95ת",
        "updatedmarker": "עודכן מאז ביקורך האחרון",
        "printableversion": "גרסת הדפסה",
        "permalink": "קישור קבוע",
        "currentevents-url": "Project:אקטואליה",
        "disclaimers": "הבהרה משפטית",
        "disclaimerpage": "Project:הבהרה משפטית",
-       "edithelp": "×¢×\96ר×\94 ×\9cעריכה",
+       "edithelp": "×¢×\96ר×\94 ×\91עריכה",
        "helppage-top-gethelp": "עזרה",
        "mainpage": "עמוד ראשי",
        "mainpage-description": "עמוד ראשי",
        "password-change-forbidden": "אין באפשרותך לשנות סיסמאות באתר זה.",
        "externaldberror": "אירעה שגיאת אימות בבסיס הנתונים, או שאינך מורשה לעדכן את החשבון החיצוני שלך.",
        "login": "כניסה לחשבון",
+       "login-security": "נא לאמת את זהותך",
        "nav-login-createaccount": "כניסה לחשבון / הרשמה",
        "userlogin": "כניסה לחשבון / הרשמה",
        "userloginnocreate": "כניסה לחשבון",
        "userlogin-resetpassword-link": "שכחת את הסיסמה?",
        "userlogin-helplink2": "עזרה בכניסה לחשבון",
        "userlogin-loggedin": "אתם כבר מחוברים לחשבון {{GENDER:$1|$1}}.\nהשתמשו בטופס שלהלן כדי להתחבר לחשבון אחר.",
+       "userlogin-reauth": "עליכם להיכנס לחשבון כדי לאמת שאתם באמת {{GENDER:$1|$1}}.",
        "userlogin-createanother": "יצירת חשבון אחר",
        "createacct-emailrequired": "כתובת דוא\"ל",
        "createacct-emailoptional": "כתובת דוא\"ל (לא חובה)",
        "createacct-email-ph": "יש להקליד את כתובת הדוא\"ל שלך",
        "createacct-another-email-ph": "יש להקליד כתובת דוא\"ל",
        "createaccountmail": "שימוש בסיסמה זמנית אקראית ושליחתה לכתובת הדוא\"ל שצוינה",
+       "createaccountmail-help": "יכול לשמש ליצירת חשבון לאדם אחר בלי לברר מה הססמה.",
        "createacct-realname": "שם אמיתי (לא חובה)",
        "createaccountreason": "סיבה:",
        "createacct-reason": "סיבה",
        "createacct-reason-ph": "סיבה ליצירת חשבון נוסף",
+       "createacct-reason-help": "הודעה שמוצגת ביומן רישום המשתמשים",
        "createacct-submit": "יצירת החשבון שלך",
        "createacct-another-submit": "יצירת חשבון",
+       "createacct-continue-submit": "המשך ביצירת החשבון",
+       "createacct-another-continue-submit": "המשך ביצירת החשבון",
        "createacct-benefit-heading": "אנשים כמוך יוצרים את {{SITENAME}}.",
        "createacct-benefit-body1": "{{PLURAL:$1|עריכה|עריכות}}",
        "createacct-benefit-body2": "{{PLURAL:$1|דף|דפים}}",
        "nocookiesnew": "חשבון המשתמש שלכם נוצר, אך לא נכנסתם כמשתמשים רשומים.\nכדי להכניס משתמשים למערכת עושה {{SITENAME}} שימוש בעוגיות.\nבדפדפן שלכם העוגיות מבוטלות.\nאנא הפעילו אותן מחדש, ולאחר מכן תוכלו להיכנס למערכת עם שם המשתמש והסיסמה החדשים שלכם.",
        "nocookieslogin": "{{SITENAME}} משתמש בעוגיות כדי להכניס משתמשים למערכת.\nבדפדפן שלכם העוגיות מבוטלות.\nאנא הפעילו אותן מחדש ונסו שוב.",
        "nocookiesfornew": "חשבון המשתמש לא נוצר, כיוון שלא יכולנו לוודא את מקורו.\nודאו שהעוגיות מופעלות בדפדפן שלכם, העלו מחדש דף זה ונסו שוב.",
+       "createacct-loginerror": "החשבון נוצר בהצלחה, אבל לא ניתן היה להיכנס אליו באופן אוטומטי. נא [[Special:UserLogin|להיכנס באופן ידני]].",
        "noname": "לא הכנסת שם משתמש תקין",
        "loginsuccesstitle": "נכנסת לחשבון",
        "loginsuccess": "'''נכנסת ל{{grammar:תחילית|{{SITENAME}}}} בשם \"$1\".'''",
-       "nosuchuser": "×\90×\99×\9f ×\9eשת×\9eש ×\91ש×\9d \"$1\".\n×\90× ×\90 ×\95×\93×\90×\95 ×©×\94×\90×\99×\95ת × ×\9b×\95×\9f (×\9b×\95×\9c×\9c ×\90×\95ת×\99×\95ת ×¨×\99ש×\99×\95ת ×\95ק×\98× ×\95ת), או [[Special:CreateAccount|צרו חשבון חדש]].",
+       "nosuchuser": "×\90×\99×\9f ×\9eשת×\9eש ×\91ש×\9d \"$1\".\nש×\99×\9e×\95 ×\9c×\91 ×©×©×\9e×\95ת ×\9eשת×\9eש×\99×\9d ×\94×\9d ×ª×\9c×\95×\99×\99×\9d־ר×\99ש×\99×\95ת.\n×\90× ×\90 ×\91Ö´×\93ק×\95 ×\90ת ×\94×\90×\99×\95ת ×©×\9c ×©×\9d ×\94×\9eשת×\9eש, או [[Special:CreateAccount|צרו חשבון חדש]].",
        "nosuchusershort": "אין משתמש בשם \"$1\".\nאנא ודאו שהאיות נכון.",
        "nouserspecified": "יש לציין שם משתמש.",
        "login-userblocked": "משתמש זה חסום. אינכם מורשים להיכנס לחשבון.",
        "createacct-another-realname-tip": "השם האמיתי הוא אופציונאלי.\nאם תבחרו לספקו, הוא ישמש לייחוס עבודת המשתמש אליו.",
        "pt-login": "כניסה לחשבון",
        "pt-login-button": "כניסה לחשבון",
+       "pt-login-continue-button": "המשך כניסה לחשבון",
        "pt-createaccount": "יצירת חשבון",
        "pt-userlogout": "יציאה מהחשבון",
        "php-mail-error-unknown": "שגיאה לא ידועה בפונקציה mail()‎ של PHP",
        "botpasswords-invalid-name": "שם המשתמש שניתן אינו מכיל את תו הפרדת ססמאות הבוט (\"$1\").",
        "botpasswords-not-exist": "למשתמש \"$1\" אין ססמת בוט בשם \"$2\".",
        "resetpass_forbidden": "לא ניתן לשנות סיסמאות.",
+       "resetpass_forbidden-reason": "לא ניתן לשנות את הסיסמאות: $1",
        "resetpass-no-info": "נדרשת כניסה לחשבון כדי לגשת לדף זה באופן ישיר.",
        "resetpass-submit-loggedin": "שינוי סיסמה",
        "resetpass-submit-cancel": "ביטול",
        "passwordreset-emailsentusername": "אם יש כתובת דואר אלקטרוני שמשויכת לשם המשתמש הזה, אז יישלח דואר אלקטרוני לאיפוס הסיסמה.",
        "passwordreset-emailsent-capture": "נשלח דואר אלקטרוני לאיפוס הסיסמה, והוא מוצג להלן.",
        "passwordreset-emailerror-capture": "נוצר דואר אלקטרוני לאיפוס הסיסמה, והוא מוצג להלן, אך שליחתו ל{{GENDER:$2|משתמש|משתמשת}} נכשלה: $1",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|דוא\"ל איפוס הסיסמה נשלח|הודעות דוא\"ל של איפוס הסיסמה נשלחו}}. {{PLURAL:$1|שם המשתמשים והסיסמה מוצגים|רשימה של שמות המשתמשים והסיסמאות מוצגת}} להלן.",
+       "passwordreset-emailerror-capture2": "לא ניתן היה לשלוח דוא\"ל ל{{GENDER:$2|משתמש|משתמשת}}: $1 {{PLURAL:$3|שם המשתמש והסיסמה מוצגים|רשימה של שמות המשתמשים והסיסמאות מוצגת}} להלן.",
+       "passwordreset-nocaller": "חובה לתת קורא",
+       "passwordreset-nosuchcaller": "הוקרא אינו קיים: $1",
+       "passwordreset-ignored": "איפוס הסיסמה לא בוצע. ייתכן שלא הוגדר ספק.",
+       "passwordreset-invalideamil": "כתובת דוא\"ל לא תקינה",
+       "passwordreset-nodata": "לא סופק שם משתמש או כתובת דוא\"ל",
        "changeemail": "שינוי או הסרת כתובת דוא\"ל",
        "changeemail-header": "יש למלא את הטופס הזה כדי לשנות את כתובת הדוא\"ל שלך. אם ברצונך להימנע משיוך כתובת דוא\"ל כלשהי לחשבון שלך, יש להשאיר את שדה כתובת הדוא\"ל החדשה ריק בעת שליחת הטופס.",
        "changeemail-passwordrequired": "יש להקליד את הסיסמה שלך כדי לאשר את השינוי.",
        "accmailtext": "סיסמה אקראית עבור [[User talk:$1|$1]] נשלחה אל $2. ניתן לשנותה בדף '''[[Special:ChangePassword|שינוי הסיסמה]]''' לאחר הכניסה.",
        "newarticle": "(חדש)",
        "newarticletext": "הגעתם לדף שעדיין אינו קיים.\nכדי ליצור את הדף הזה, התחילו להקליד בתיבת הטקסט שלמטה (ראו את [$1 דף העזרה] למידע נוסף).\nאם הגעתם לכאן בטעות, לחצו על כפתור ה<strong>חזרה</strong> (Back) בדפדפן שלכם.",
-       "anontalkpagetext": "----\n<em>זהו דף שיחה של משתמש אנונימי שעדיין לא יצר חשבון במערכת, או שהוא לא משתמש בו.</em>\nלכן עלינו להשתמש בכתובת ה־IP המספרית כדי לזהותו.\nייתכן שכתובת IP זו תהיה משותפת למספר משתמשים.\nאם אתם משתמשים אנונימיים ומרגישים שקיבלתם הודעות בלתי רלוונטיות, אנא [[Special:CreateAccount|צרו חשבון]] או [[Special:UserLogin|היכנסו לחשבון]] כדי להימנע מבלבול עתידי עם משתמשים אנונימיים נוספים.",
+       "anontalkpagetext": "----\n<em>זהו דף שיחה של משתמש אנונימי שעדיין לא יצר חשבון במערכת, או שהוא לא משתמש בו.</em>\nלכן עלינו להשתמש בכתובת ה־IP המספרית כדי לזהותו.\nייתכן שכתובת IP זו תהיה משותפת למספר משתמשים.\nאם אתם משתמשים אנונימיים ומרגישים שקיבלתם הודעות בלתי רלוונטיות, אנא [[Special:CreateAccount|צרו חשבון]] או [[Special:UserLogin|היכנסו לחשבון]] כדי להימנע מבלבולים עתידיים עם משתמשים אנונימיים נוספים.",
        "noarticletext": "אין כרגע טקסט בדף הזה.\nבאפשרותך [[Special:Search/{{PAGENAME}}|לחפש את כותרת הדף]] בדפים אחרים,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} לחפש את הדף ביומנים],\nאו [{{fullurl:{{FULLPAGENAME}}|action=edit}} ליצור את הדף]</span>.",
        "noarticletext-nopermission": "אין כרגע טקסט בדף הזה.\nבאפשרותך [[Special:Search/{{PAGENAME}}|לחפש את כותרת הדף]] בדפים אחרים או <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} לחפש את הדף ביומנים]</span>, אך אין לך הרשאה ליצור את הדף.",
        "missing-revision": "גרסה #$1 של הדף \"{{FULLPAGENAME}}\" אינה קיימת.\n\nזה נגרם בדרך כלל על־ידי לחיצה על קישור ישן לגרסה קודמת של דף שנמחק.\nאפשר למצוא פרטים ב[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} יומן המחיקות].",
        "prefs-rc": "שינויים אחרונים",
        "prefs-watchlist": "רשימת המעקב",
        "prefs-editwatchlist": "עריכת רשימת המעקב",
-       "prefs-editwatchlist-label": "עריכת דפים ברשימת המעקב:",
+       "prefs-editwatchlist-label": "עריכת דפים ברשימת המעקב שלך:",
        "prefs-editwatchlist-edit": "הצגה או הסרה של דפים מרשימת המעקב",
        "prefs-editwatchlist-raw": "עריכת רשימת המעקב הגולמית",
        "prefs-editwatchlist-clear": "ניקוי רשימת המעקב",
        "wlheader-showupdated": "דפים שהשתנו מאז ביקורך האחרון בהם מוצגים ב'''הדגשה'''.",
        "wlnote": "להלן {{PLURAL:$1|השינוי האחרון|<strong>$1</strong> השינויים האחרונים}} {{PLURAL:$2|בשעה האחרונה|בשעתיים האחרונות|ב־<strong>$2</strong> השעות האחרונות}}, עד $4, $3.",
        "wlshowlast": "הצגת $1 שעות אחרונות $2 ימים אחרונים",
-       "watchlist-hide": "×\94סתר×\94",
+       "watchlist-hide": "×\94סתרת",
        "watchlist-submit": "הצגה",
        "wlshowtime": "תקופת זמן לצפייה:",
        "wlshowhideminor": "עריכות משניות",
        "mycontris": "תרומות",
        "anoncontribs": "תרומות",
        "contribsub2": "עבור {{GENDER:$3|$1}} ($2)",
-       "contributions-userdoesnotexist": "×\94×\97ש×\91×\95×\9f \"$1\" אינו רשום.",
+       "contributions-userdoesnotexist": "×\97ש×\91×\95×\9f ×\94×\9eשת×\9eש \"$1\" אינו רשום.",
        "nocontribs": "לא נמצאו שינויים המתאימים לקריטריונים אלו.",
        "uctop": "(נוכחי)",
        "month": "עד החודש:",
        "ipb-confirmaction": "אם אתם באמת בטוחים שברצונכם לעשות זאת, אנא סמנו את השדה \"{{int:ipb-confirm}}\" שבתחתית.",
        "ipb-edit-dropdown": "עריכת סיבות החסימה",
        "ipb-unblock-addr": "ביטול חסימה של $1",
-       "ipb-unblock": "×\94סרת חסימה של שם משתמש או כתובת IP",
+       "ipb-unblock": "×\91×\99×\98×\95×\9c חסימה של שם משתמש או כתובת IP",
        "ipb-blocklist": "הצגת החסימות הנוכחיות",
        "ipb-blocklist-contribs": "תרומות של {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "נותרו $1",
        "tooltip-pt-logout": "יציאה מהחשבון",
        "tooltip-pt-createaccount": "מומלץ ליצור חשבון ולהיכנס אליו; עם זאת, אין חובה לעשות זאת",
        "tooltip-ca-talk": "שיחה על דף זה",
-       "tooltip-ca-edit": "עריכת דף זה באמצעות קוד ויקי",
+       "tooltip-ca-edit": "עריכת דף זה",
        "tooltip-ca-addsection": "הוספת פסקה חדשה",
-       "tooltip-ca-viewsource": "×\93×£ ×\96×\94 ×\9e×\95×\92×\9f.\n×\91×\90פשר×\95ת×\9a ×\9cצפ×\95ת ×\91×\98קס×\98 המקור שלו",
+       "tooltip-ca-viewsource": "×\93×£ ×\96×\94 ×\9e×\95×\92×\9f.\n×\91×\90פשר×\95ת×\9a ×\9cצפ×\95ת ×\91ק×\95×\93 המקור שלו",
        "tooltip-ca-history": "גרסאות קודמות של דף זה",
-       "tooltip-ca-protect": "הגנה על דף זה",
+       "tooltip-ca-protect": "×\94פע×\9cת ×\94×\92× ×\94 ×¢×\9c ×\93×£ ×\96×\94",
        "tooltip-ca-unprotect": "שינוי ההגנה על דף זה",
        "tooltip-ca-delete": "מחיקת דף זה",
        "tooltip-ca-undelete": "שחזור עריכות שנעשו בדף זה לפני שנמחק",
        "tooltip-ca-move": "העברת דף זה",
-       "tooltip-ca-watch": "הוספת דף זה לרשימת המעקב",
-       "tooltip-ca-unwatch": "הסרת דף זה מרשימת המעקב",
+       "tooltip-ca-watch": "הוספת דף זה לרשימת המעקב שלך",
+       "tooltip-ca-unwatch": "הסרת דף זה מרשימת המעקב שלך",
        "tooltip-search": "חיפוש ב{{grammar:תחילית|{{SITENAME}}}}",
        "tooltip-search-go": "מעבר לדף בשם הזה בדיוק, אם הוא קיים",
        "tooltip-search-fulltext": "חיפוש טקסט זה בדפים",
        "watchlisttools-clear": "ניקוי רשימת המעקב",
        "watchlisttools-view": "הצגת השינויים הרלוונטיים",
        "watchlisttools-edit": "הצגה ועריכה של רשימת המעקב",
-       "watchlisttools-raw": "ער×\99×\9bת ×\94רש×\99×\9e×\94 הגולמית",
+       "watchlisttools-raw": "ער×\99×\9bת ×¨×©×\99×\9eת ×\94×\9eעק×\91 הגולמית",
        "iranian-calendar-m1": "פרברדין",
        "iranian-calendar-m2": "אורדיבהשט",
        "iranian-calendar-m3": "חורדאד",
        "log-action-filter-suppress-block": "העלמות של משתמשים באמצעות חסימה",
        "log-action-filter-suppress-reblock": "העלמות של משתמשים באמצעות חסימה מחדש",
        "log-action-filter-upload-upload": "העלאות חדשות",
-       "log-action-filter-upload-overwrite": "דריסת קבצים קיימים"
+       "log-action-filter-upload-overwrite": "דריסת קבצים קיימים",
+       "authmanager-authn-not-in-progress": "האימות נכשל או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
+       "authmanager-authn-no-primary": "לא ניתן היה לאמת את האישורים שסופקו.",
+       "authmanager-authn-no-local-user": "האישורים שסופקו אינם שייכים לשום משתמש באתר זה.",
+       "authmanager-authn-no-local-user-link": "נתוני ההאמנה שניתנו תקינים, אבל אינם משויכים לשום משתמש בוויקי הזה. נא להיכנס לחשבון באופן שונה, או ליצור משתמש חדש ואז תהיה לך אפשרות לקשר את נתוני ההאמנה הקודמים שלך לחשבון ההוא.",
+       "authmanager-authn-autocreate-failed": "יצירה אוטומטית של חשבון מקומי נכשלה: $1",
+       "authmanager-change-not-supported": "לא ניתן לשנות את נתוני ההאמנה שניתנו, כי שום דבר לא ישתמש בהם.",
+       "authmanager-create-disabled": "אפשרות יצירת החשבונות מבוטלת.",
+       "authmanager-create-from-login": "כדי ליצור את החשבון, נא למלא את השדות שלהלן.",
+       "authmanager-create-not-in-progress": "יצירת החשבון נכשלה או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
+       "authmanager-create-no-primary": "האישורים שסופקו לא יכולים להיות בשימוש ביצירת חשבון.",
+       "authmanager-link-no-primary": "האישורים שסופקו לא יכולים להיות בשימוש בקישור חשבונות.",
+       "authmanager-link-not-in-progress": "קישור החשבונות נכשל או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
+       "authmanager-authplugin-setpass-failed-title": "שינוי הסיסמה נכשל",
+       "authmanager-authplugin-setpass-failed-message": "תוסף האימות דחה את שינוי הסיסמה.",
+       "authmanager-authplugin-create-fail": "תוסף האימות דחה את יצירת החשבון.",
+       "authmanager-authplugin-setpass-denied": "תוסף האימות אינו מאפשר לשנות סיסמאות.",
+       "authmanager-authplugin-setpass-bad-domain": "דומיין לא תקין.",
+       "authmanager-autocreate-noperm": "אין אפשרות ליצור חשבונות באופן אוטומטי.",
+       "authmanager-autocreate-exception": "יצירת חשבונות אוטומטית מבוטלת באופן אוטומטי בשל שגיאות קודמות.",
+       "authmanager-userdoesnotexist": "חשבון המשתמש \"$1\" אינו רשום.",
+       "authmanager-userlogin-remembermypassword-help": "האם לזכור את הסיסמה למשך זמן ארוך יותר מאורך הפעולה.",
+       "authmanager-username-help": "שם המשתמש לאימות.",
+       "authmanager-password-help": "הסיסמה לאימות.",
+       "authmanager-domain-help": "שם מתחם לאימות חיצוני.",
+       "authmanager-retype-help": "חזרה על הסיסמה.",
+       "authmanager-email-label": "דוא\"ל",
+       "authmanager-email-help": "כתובת דוא\"ל",
+       "authmanager-realname-label": "שם אמיתי",
+       "authmanager-realname-help": "השם האמיתי של המשתמש",
+       "authmanager-provider-password": "אימות שמבוסס על סיסמה",
+       "authmanager-provider-password-domain": "אימות מבוסס מתחם וססמה.",
+       "authmanager-provider-temporarypassword": "סיסמה זמנית",
+       "authprovider-confirmlink-message": "בהתבסס על ניסיונות הכניסה האחרונים שלך, ניתן לקשר את החשבונות הבאים לחשבון שלך. לאחר שהחשבונות יקושרו, ניתן יהיה להיכנס לחשבון באמצעותם. נא לבחור את החשבונות שברצונך לקשר.",
+       "authprovider-confirmlink-request-label": "החשבונות שיקושרו",
+       "authprovider-confirmlink-success-line": "$1: הקישור בוצע בהצלחה.",
+       "authprovider-confirmlink-failed": "קישור החשבונות לא הושלם: $1",
+       "authprovider-confirmlink-ok-help": "להמשיך אחרי הודעות שגיאת קישור.",
+       "authprovider-resetpass-skip-label": "דילוג",
+       "authprovider-resetpass-skip-help": "לדלג על איפוס הסיסמה.",
+       "authform-nosession-login": "האימות הושלם בהצלחה, אבל הדפדפן שלך אינו \"זוכר\" את הכניסה שלך לחשבון.\n\n$1",
+       "authform-nosession-signup": "החשבון נוצר, אבל הדפדפן שלך אינו \"זוכר\" את הכניסה שלך לחשבון.\n\n$1",
+       "authform-newtoken": "אסימון חסר. $1",
+       "authform-notoken": "אסימון חסר",
+       "authform-wrongtoken": "אסימון שגוי",
+       "specialpage-securitylevel-not-allowed-title": "הגישה נדחתה",
+       "specialpage-securitylevel-not-allowed": "מצטערים, אין באפשרותך להשתמש בדף זה כי הזהות שלך לא אומתה.",
+       "authpage-cannot-login": "לא ניתן להתחיל את תהליך הכניסה לחשבון.",
+       "authpage-cannot-login-continue": "לא ניתן היה להיכנס לחשבון. כנראה שזמן ההמתנה של הפעולה חלף.",
+       "authpage-cannot-create": "לא ניתן להתחיל את תהליך יצירת החשבון.",
+       "authpage-cannot-create-continue": "לא ניתן להמשיך בתהליך יצירת החשבון. כנראה שזמן ההמתנה של הפעולה חלף.",
+       "authpage-cannot-link": "לא ניתן להתחיל את תהליך קישור החשבונות.",
+       "authpage-cannot-link-continue": "לא ניתן להמשיך בתהליך קישור החשבונות. כנראה שזמן ההמתנה של הפעולה חלף.",
+       "cannotauth-not-allowed-title": "הגישה נדחתה",
+       "cannotauth-not-allowed": "אינך מורשה להשתמש בדף זה",
+       "changecredentials": "שינוי האישורים",
+       "changecredentials-submit": "שינוי",
+       "changecredentials-submit-cancel": "ביטול",
+       "changecredentials-invalidsubpage": "$1 אינו סוג אישור תקין.",
+       "changecredentials-success": "האישורים שלך שונו.",
+       "removecredentials": "הסרת האישורים",
+       "removecredentials-submit": "הסרה",
+       "removecredentials-submit-cancel": "ביטול",
+       "removecredentials-invalidsubpage": "$1 אינו סוג אישור תקין.",
+       "removecredentials-success": "האישורים שלך הוסרו.",
+       "credentialsform-provider": "סוג האישורים:",
+       "credentialsform-account": "שם החשבון:",
+       "cannotlink-no-provider-title": "אין חשבונות שניתן לקשר",
+       "cannotlink-no-provider": "אין חשבונות שניתן לקשר.",
+       "linkaccounts": "קישור חשבונות",
+       "linkaccounts-success-text": "החשבון קושר.",
+       "linkaccounts-submit": "קישור החשבונות",
+       "unlinkaccounts": "ביטול הקישור של החשבונות",
+       "unlinkaccounts-success": "קישור החשבון בוטל."
 }
index 7c5e869..330e083 100644 (file)
        "botpasswords-invalid-name": "Il nome utente indicato non contiene il separatore per password bot (\"$1\").",
        "botpasswords-not-exist": "L'utente \"$1\" non dispone di una password bot chiamata \"$2\".",
        "resetpass_forbidden": "Non è possibile modificare le password",
+       "resetpass_forbidden-reason": "Non è possibile modificare le password: $1",
        "resetpass-no-info": "Devi aver effettuato l'accesso per accedere a questa pagina direttamente.",
        "resetpass-submit-loggedin": "Cambia password",
        "resetpass-submit-cancel": "Annulla",
        "accmailtext": "Una password generata casualmente per [[User talk:$1|$1]] è stata inviata a $2. Questa password può essere modificata nella pagina per ''[[Special:ChangePassword|cambiare la password]]'' subito dopo l'accesso.",
        "newarticle": "(Nuovo)",
        "newarticletext": "Il collegamento appena seguito corrisponde ad una pagina non ancora esistente.\nSe vuoi creare la pagina ora, basta cominciare a scrivere il testo nella casella qui sotto (vedi la [$1 pagina di aiuto] per maggiori informazioni).\nSe il collegamento è stato aperto per errore, è sufficiente fare clic sul pulsante <strong>Indietro</strong> del proprio browser.",
-       "anontalkpagetext": "----\n''Questa è la pagina di discussione di un utente anonimo, che non ha ancora creato un'utenza o comunque non la sta usando. Per identificarlo è quindi necessario usare il numero del suo indirizzo IP. Gli indirizzi IP possono però essere condivisi da più utenti. Se sei un utente anonimo e ritieni che i commenti presenti in questa pagina non si riferiscano a te, [[Special:CreateAccount|crea una nuova utenza]] o [[Special:UserLogin|entra con quella che già hai]] per evitare di essere confuso con altri utenti anonimi in futuro.''",
+       "anontalkpagetext": "----\n<em>Questa è la pagina di discussione di un utente anonimo, che non ha ancora creato un'utenza o comunque non la sta usando.</em>\nPer identificarlo è quindi necessario usare il numero del suo indirizzo IP.\nGli indirizzi IP possono però essere condivisi da più utenti.\nSe sei un utente anonimo e ritieni che i commenti presenti in questa pagina non si riferiscano a te, [[Special:CreateAccount|crea una nuova utenza]] o [[Special:UserLogin|entra con quella che già hai]] per evitare di essere confuso con altri utenti anonimi in futuro.",
        "noarticletext": "In questo momento la pagina richiesta è vuota.\nPuoi [[Special:Search/{{PAGENAME}}|cercare questo titolo]] nelle altre pagine del sito, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercare nei registri correlati] oppure [{{fullurl:{{FULLPAGENAME}}|action=edit}} creare questa pagina]</span>.",
        "noarticletext-nopermission": "In questo momento la pagina richiesta è vuota. È possibile [[Special:Search/{{PAGENAME}}|cercare questo titolo]] nelle altre pagine del sito o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} cercare nei registri correlati]</span>, ma non hai i permessi per creare questa pagina.",
        "missing-revision": "La versione #$1 della pagina \"{{FULLPAGENAME}}\" non esiste.\n\nQuesto si verifica solitamente seguendo un collegamento a una pagina cancellata, in una cronologia non aggiornata.\nI dettagli possono essere trovati nel [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro delle cancellazioni].",
        "log-action-filter-suppress-block": "Soppressione utente da blocco",
        "log-action-filter-suppress-reblock": "Soppressione utente da ri-blocco",
        "log-action-filter-upload-upload": "Nuovo caricamento",
-       "log-action-filter-upload-overwrite": "Ricaricamento"
+       "log-action-filter-upload-overwrite": "Ricaricamento",
+       "authmanager-userdoesnotexist": "L'utenza \"$1\" non è registrata.",
+       "authmanager-email-help": "Indirizzo email",
+       "authmanager-realname-help": "Nome reale dell'utente",
+       "authform-newtoken": "Token mancante. $1",
+       "changecredentials-submit-cancel": "Annulla",
+       "removecredentials-submit": "Rimuovi",
+       "removecredentials-submit-cancel": "Annulla"
 }
index 874055f..553dd36 100644 (file)
        "summary-preview": "Prawuryan tingkesan:",
        "subject-preview": "Prawuryaning jejer:",
        "blockedtitle": "Panganggo kapalangan",
-       "blockedtext": "'''Asma panganggo utawa alamat IP panjenengan diblokir.'''\n\nBlokir iki sing nglakoni $1.\nAlesané ''$2''.\n\n* Diblokir wiwit: $8\n* Kadaluwarsa pemblokiran ing: $6\n* Sing arep diblokir: $7\n\nPanjenengan bisa ngubungi $1 utawa [[{{MediaWiki:Grouppage-sysop}}|pangurus liyané]] kanggo ngomongaké prakara iki.\n\nPanjenengan ora bisa nggunakaké fitur 'Kirim layang e-mail panganggo iki' kejaba panjenengan wis nglebokaké alamat e-mail sing sah ing [[Special:Preferences|préferènsi]] panjenengan.\n\nAlamat IP panjenengan iku $3, lan ID pamblokiran iku #$5.\nTulung kabèh informasi ing ndhuwur iki disertakaké ing saben pitakon panjenengan.",
-       "autoblockedtext": "Alamat IP panjenangan wis diblokir minangka otomatis amerga dienggo déning panganggo liyané. Pamblokiran dilakoni déning $1 mawa alesan:\n\n:''$2''\n\n* Diblokir wiwit: $8\n* Blokir kadaluwarsa ing: $6\n* Sing dikarepaké diblokir: $7\n\nPanjenengan bisa ngubungi $1 utawa [[{{MediaWiki:Grouppage-sysop}}|pangurus liyané]] kanggo ngomongaké perkara iki.\n\nPanjenengan ora bisa nganggo fitur \"kirim e-mail panganggo iki\" kejaba panjenengan wis nglebokaké alamat e-mail sing sah ing [[Special:Preferences|préferènsi]] panjenengan lan panjenengan wis diblokir kanggo nggunakaké.\n\nID pamblokiran panjenengan iku #$5 lan alamat IP panjenengan iku $3. Tulung sertakna informasi ing dhuwur kabèh iki saben ngajokaké pitakonan panjenengan. Matur nuwun.",
+       "blockedtext": "<b>Asma panganggo utawa alamat IP panjenengan diblokir.</b>\n\nBlokir iki sing nglakoni $1.\nAlesané <i>$2</i>.\n\n* Diblokir wiwit: $8\n* Kadaluwarsa pemblokiran ing: $6\n* Sing arep diblokir: $7\n\nPanjenengan bisa ngubungi $1 utawa [[{{MediaWiki:Grouppage-sysop}}|pangurus liyané]] kanggo ngomongaké prakara iki.\n\nPanjenengan ora bisa nggunakaké fitur 'Kirim layang é-mail panganggo iki' kejaba panjenengan wis nglebokaké alamat é-mail sing sah ing [[Special:Preferences|prèferènsi]] panjenengan.\n\nAlamat IP panjenengan iku $3, lan ID pamblokiran iku #$5.\nTulung kabèh informasi ing ndhuwur iki disertakaké ing saben pitakon panjenengan.",
+       "autoblockedtext": "Alamat IP panjenangan wis diblokir minangka otomatis amerga dienggo déning panganggo liyané. Pamblokiran dilakoni déning $1 mawa alesan:\n\n:''$2''\n\n* Diblokir wiwit: $8\n* Blokir kadaluwarsa ing: $6\n* Sing dikarepaké diblokir: $7\n\nPanjenengan bisa ngubungi $1 utawa [[{{MediaWiki:Grouppage-sysop}}|pangurus liyané]] kanggo ngomongaké perkara iki.\n\nPanjenengan ora bisa nganggo fitur \"kirim e-mail panganggo iki\" kejaba panjenengan wis nglebokaké alamat e-mail sing sah ing [[Special:Preferences|prèferènsi]] panjenengan lan panjenengan wis diblokir kanggo nggunakaké.\n\nID pamblokiran panjenengan iku #$5 lan alamat IP panjenengan iku $3. Tulung sertakna informasi ing dhuwur kabèh iki saben ngajokaké pitakonan panjenengan. Matur nuwun.",
        "blockednoreason": "ora ana alesan sing diwènèhaké",
        "whitelistedittext": "Sampéyan kudu $1 murih bisa mbesut kaca.",
        "confirmedittext": "Panjenengan kudu ndhedhes alamat e-mail dhisik sadurungé pareng nyunting sawijining kaca. Mangga nglebokaké lan validasi alamat e-mail panjenengan sadurungé nglakoni panyuntingan. Alamat e-mail sawisé bisa diowahi liwat [[Special:Preferences|kaca préférènsi]]",
        "recentchangescount": "Cacahing besutan sing dituduhaké kanthi baku:",
        "prefs-help-recentchangescount": "Iki klebu owah-owahan pungkasan, kaca sajarah, lan log.",
        "prefs-help-watchlist-token2": "Ini adalah kunci rahasia (token) ke web feed dari daftar pantauan Anda.\nSiapa saja yang tahu akan dapat melihat daftar pantauan Anda, jadi jangan dibagikan.\n[[Special:ResetTokens|Klik di sini jika Anda perlu menyetel ulang]].",
-       "savedprefs": "Préferènsi Panjenengan wis disimpen",
+       "savedprefs": "Prèferènsi Panjenengan wis disimpen",
        "savedrights": "Haking panganggo {{GENDER:$1|$1}} wis kasimpen.",
        "timezonelegend": "Zona wektu:",
        "localtime": "Wektu saenggon:",
        "right-edituserjs": "Besut barkas-barkas JavaScript panganggo liya",
        "right-editmyusercss": "Owahi berkas CSS panganggo sampeyan",
        "right-editmyuserjs": "Owahi berkas JavaScript panganggo sampeyan",
-       "right-viewmywatchlist": "Dheleng daftar pangawasan sampeyan",
+       "right-viewmywatchlist": "Deleng pawawanganing sampéyan",
        "right-editmywatchlist": "Owahi daftar pangawasan sampeyan. Cathetan: ana cara liyane kanggo nambahi kaca menyang daftar, sanadyan ora duwe hak iki.",
        "right-viewmyprivateinfo": "Dheleng data pribadi sampeyan (kayata alamat layang elektronik, jeneng asli)",
        "right-editmyprivateinfo": "Owahi data pribadi sampeyan (kayata alamat layang elektronik, jeneng asli)",
        "usermessage-summary": "Tinggalaké layang sistem.",
        "usermessage-editor": "Pawartaning layang sistem",
        "watchlist": "Daptar pangawasan",
-       "mywatchlist": "Daftar pangawasan",
+       "mywatchlist": "Pawawangan",
        "watchlistfor2": "Kanggo $1 $2",
-       "nowatchlist": "Daftar pangawasan panjenengan kosong.",
+       "nowatchlist": "Ora ana apa-apa ing pawawanganing sampéyan.",
        "watchlistanontext": "Mangga $1 kanggo mirsani utawa nyunting daftar pangawasan panjenengan.",
        "watchnologin": "Durung mlebu log",
        "addwatch": "Tambah nèng daptar pangawasan",
        "wlheader-showupdated": "Kaca-kaca sing wis owah wiwit ditiliki panjenengan kaping pungkasan, dituduhaké mawa '''aksara kandel'''",
        "wlnote": "Ngisor iki {{PLURAL:$1|owahan pungkasan|'''$1''' owahan pungkasan}} {{PLURAL:$2|jam|'''$2''' jam}} kapungkur, per $3, $4.",
        "wlshowlast": "Tuduhna $1 jam $2 dina  pungkasan",
-       "watchlist-options": "Opsi daftar pangawasan",
+       "watchlist-options": "Pilihaning pawawangan",
        "watching": "Ngawasi...",
        "unwatching": "Ngilangi pangawasan...",
        "watcherrortext": "Ana kasalahan nalika ngganti pangaturan daptar pangawasan Sampéyan kanggo \"$1\".",
        "ipbnounblockself": "Sampéyan ora dililakaké mbukak blokirané Sampéyan",
        "lockdb": "Kunci basis data",
        "unlockdb": "Buka kunci basis data",
-       "lockdbtext": "Ngunci basis data bakal menggak kabèh panganggo kanggo nyunting kaca, ngowahi préferènsi panganggo, nyunting daftar pangawasan, lan prekara-prekara liyané sing merlokaké owah-owahan basis data. Pastèkna yèn iki pancèn panjenengan gayuh, lan yèn panjenengan ora lali mbuka kunci basis data sawisé pangopènan rampung.",
-       "unlockdbtext": "Mbuka kunci basis data bakal mbalèkaké kabèh panganggo bisa nyunting kaca manèh, ngowahi préferènsi panganggo, nyunting daftar pangawasan, lan prekara-prekara liyané sing merlokaké pangowahan marang basis data.\nTulung pastèkna yèn iki pancèn sing panjenengan gayuh.",
+       "lockdbtext": "Ngunci basis data bakal menggak kabèh panganggo kanggo nyunting kaca, ngowahi prèferènsi panganggo, nyunting daftar pangawasan, lan prekara-prekara liyané sing merlokaké owah-owahan basis data. Pastèkna yèn iki pancèn panjenengan gayuh, lan yèn panjenengan ora lali mbuka kunci basis data sawisé pangopènan rampung.",
+       "unlockdbtext": "Mbuka kunci basis data bakal mbalèkaké kabèh panganggo bisa nyunting kaca manèh, ngowahi prèferènsi panganggo, nyunting daftar pangawasan, lan prekara-prekara liyané sing merlokaké pangowahan marang basis data.\nTulung pastèkna yèn iki pancèn sing panjenengan gayuh.",
        "lockconfirm": "Iya, aku pancèn péngin ngunci basis data.",
        "unlockconfirm": "Iya, aku pancèn péngin tmbuka kunci basis data.",
        "lockbtn": "Kunci basis data",
        "tooltip-ca-undelete": "Balèkna suntingan ing kaca iki sadurungé kaca iki dibusak",
        "tooltip-ca-move": "Lih kaca iki",
        "tooltip-ca-watch": "Tambahaké kaca iki nyang pawawangan sapéyan",
-       "tooltip-ca-unwatch": "Busak kaca iki saka daftar pangawasan panjenengan",
+       "tooltip-ca-unwatch": "Busak kaca iki saka pawawanganing sampéyan",
        "tooltip-search": "Golèk nyang {{SITENAME}}",
        "tooltip-search-go": "Jujug kaca asesirah persis mangkéné yèn ana",
        "tooltip-search-fulltext": "Golèk kaca isi tulisan kaya mangkéné",
        "tooltip-preview": "Prawuryan owah-owahaning sampéyan. Anggoa cara iki sadurungé nyimpen.",
        "tooltip-diff": "Tuduhaké owah-owahan endi sing sampéyan gawé tumrap tulisan iki",
        "tooltip-compareselectedversions": "Delengen prabédan antara rong vèrsi kaca iki sing dipilih.",
-       "tooltip-watch": "Tambahna kaca iki ing daftar pangawasan panjenengan",
+       "tooltip-watch": "Wuwuh kaca iki nyang pawawanganing sampéyan",
        "tooltip-watchlistedit-normal-submit": "Singkiraké judhul",
        "tooltip-watchlistedit-raw-submit": "Anyari daptar pangawasan",
        "tooltip-recreate": "Gawéa kaca iki manèh senadyan tau dibusak",
        "namespacesall": "kabèh",
        "monthsall": "kabèh",
        "confirmemail": "Konfirmasi alamat e-mail",
-       "confirmemail_noemail": "Panjenengan ora maringi alamat e-mail sing absah ing [[Special:Preferences|préferènsi]] panjenengan.",
+       "confirmemail_noemail": "Panjenengan ora maringi alamat é-mail sing absah ing [[Special:Preferences|prèferènsi]] panjenengan.",
        "confirmemail_text": "{{SITENAME}} ngwajibaké panjenengan ndhedhes utawa konfirmasi alamat e-mail panjenengan sadurungé bisa nganggo fitur-fitur e-mail.\nPencèten tombol ing ngisor iki kanggo ngirim sawijining kode konfirmasi arupa sawijining pranala;\nTuladen pranala iki ing panjlajah wèb panjenengan kanggo ndhedhes yèn alamat e-mail panjenengan pancèn bener.",
        "confirmemail_pending": "Sawijining kode konfirmasi wis dikirim menyang alamat e-mail panjenengan;\nyèn panjenengan lagi waé nggawé akun utawa rékening panjenengan, mangga nunggu sawetara menit nganti layang iku tekan sadurungé nyuwun kode anyar manèh.",
        "confirmemail_send": "Kirim kode konfirmasi",
        "lag-warn-normal": "Owah-owahan pungkasan sing luwih anyar tinimbang $1 {{PLURAL:$1|detik|detik}} mbokmanawa ora metu ing pratélan iki.",
        "lag-warn-high": "Amarga gedhéné ''lag'' basis data server, owah-owahan pungkasan sing luwih anyar saka $1 {{PLURAL:$1|detik|detik}} mbokmanawa ora metu ing daftar iki.",
        "watchlistedit-normal-title": "Besut pawawangan",
-       "watchlistedit-normal-legend": "Busak irah-irahan saka daftar pangawasan",
+       "watchlistedit-normal-legend": "Busak sesirah saka pawawangan",
        "watchlistedit-normal-explain": "Irah-irahan utawa judhul ing daftar pangawasan panjenengan kapacak ing ngisor iki.\nKanggo mbusak sawijining irah-irahan, kliken kothak ing pinggiré, lan banjur kliken \"Busak judhul\".\nPanjenengan uga bisa [[Special:EditWatchlist/raw|nyunting daftar mentah]].",
        "watchlistedit-normal-submit": "Busak irah-irahan",
        "watchlistedit-normal-done": "Irah-irahan {{PLURAL:$1|siji|$1}} wis dibusak saka daftar pangawasan panjenengan:",
        "watchlistedit-raw-legend": "Besut pawawangan wantahan",
        "watchlistedit-raw-explain": "Irah-irahan ing daftar pangawasan panjenengan kapacak ing ngisor iki, lan bisa diowahi mawa nambahaké utawa mbusak daftar; sairah-irahan saban barisé.\nYèn wis rampung, anyarana kaca daftar pangawasan iki.\nPanjenengan uga bisa [[Special:EditWatchlist|nganggo éditor standar panjenengan]].",
        "watchlistedit-raw-titles": "Irah-irahan:",
-       "watchlistedit-raw-submit": "Anyarana daftar pangawasan",
-       "watchlistedit-raw-done": "Daftar pangawasan panjenengan wis dianyari.",
+       "watchlistedit-raw-submit": "Anyari pawawangan",
+       "watchlistedit-raw-done": "Pawawanganing sampéyan wis dianyari.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 irah-irahan wis|$1 irah-irahan wis}} ditambahaké:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 irah-irahan wis|$1 irah-irahan wis}} diwetokaké:",
        "watchlisttools-view": "Tuduhna owah-owahan sing ana gandhèngané",
index dbee372..ac51f0f 100644 (file)
        "showpreview": "미리 보기",
        "showdiff": "차이 보기",
        "blankarticle": "<strong>경고:</strong> 만들려는 문서가 비어 있습니다.\n\"{{int:savearticle}}\"을 다시 클릭하면, 아무 내용 없이 문서가 만들어집니다.",
-       "anoneditwarning": "<strong>경고:</strong> 로그인을 하고 있지 않습니다. 편집을 하게 되면 IP 주소가 공개적으로 보여집니다. <strong>[$1 로그인]</strong>하거나 <strong>[$2 계정을 생성하면]</strong>, 편집 시에 다른 이점과 함께 사용자 이름이 표시됩니다.",
+       "anoneditwarning": "<strong>경고:</strong> 로그인을 하고 있지 않습니다. 편집을 하면 IP 주소가 공개적으로 보이게 됩니다. <strong>[$1 로그인]</strong>하거나 <strong>[$2 계정을 생성하면]</strong>, 편집 시에 사용자 이름이 표시되며 더불어 다른 혜택들도 누릴 수 있습니다.",
        "anonpreviewwarning": "<em>로그인하고 있지 않습니다. 문서를 저장하면 당신의 IP 주소가 문서의 편집 역사에 남게 됩니다.</em>",
        "missingsummary": "'''알림:''' 편집 요약을 적지 않았습니다.\n이대로 \"{{int:savearticle}}\"을 클릭하면 편집 요약 없이 저장됩니다.",
        "selfredirect": "<strong>경고:</strong> 자기 자신으로 문서를 넘겨주고 있습니다.\n넘겨줄 대상을 잘못 입력했거나, 잘못된 문서를 편집하고 있을 수 있습니다.\n\"{{int:savearticle}}\"을 입력하면, 넘겨주기 문서가 생성될 것입니다.",
        "saturday-at": "토요일 $1",
        "sunday-at": "일요일 $1",
        "yesterday-at": "어제 $1",
-       "bad_image_list": "형식은 아래와 같습니다.\n\n\"*\"로 시작하는 목록의 내용만 적용됩니다.\n매 줄의 첫번째 링크는 부적절한 파일을 가리켜야 합니다.\n같은 줄에 따라오는 모든 링크는 예외로 봅니다. (예: 파일이 사용되어야 하는 문서)",
+       "bad_image_list": "형식은 아래와 같습니다.\n\n\"*\"로 시작하는 목록의 내용만 적용됩니다.\n매 줄의 첫 번째 링크는 부적절한 파일을 가리켜야 합니다.\n같은 줄에 따라오는 모든 링크는 예외로 봅니다. (예: 파일이 사용되어야 하는 문서)",
        "variantname-zh-hans": "간체",
        "variantname-zh-hant": "번체",
        "metadata": "메타데이터",
index 49d1068..3f3cf2b 100644 (file)
        "powersearch-togglelabel": "Selekteren:",
        "powersearch-toggleall": "Alle",
        "powersearch-togglenone": "Gien",
+       "powersearch-remember": "Keuze onthouwen veur toekomstige zeukopdrachten",
        "search-external": "Extern zeuken",
        "searchdisabled": "Zeuken in {{SITENAME}} is niet meugelik. Je kunnen gebruukmaken van Google. De gegevens over {{SITENAME}} bin misschien niet bie-ewörken.",
        "search-error": "Der is wat mis-egaon bie t zeuken: $1",
index aa9052c..9ba39f7 100644 (file)
        "actions": "Akschonen",
        "namespaces": "Naamrüüm",
        "variants": "Varianten",
+       "navigation-heading": "Navigatschoonsmenü",
        "errorpagetitle": "Fehler",
        "returnto": "Trüch to $1.",
        "tagline": "Vun {{SITENAME}}",
        "newarticle": "(Nee)",
        "newarticletext": "Du büst op en Sied kamen, de dat noch nich gifft.\nWenn du disse Sied opstellen wullt, schriev dien Text in dat Finster ünnen  (kiek op de [$1 Hülpsied] för mehr Infos).\nWenn du de Sied gornich ännern wullst, denn klick op den '''Trügg'''-Knoop in dien Webkieker.",
        "anontalkpagetext": "---- ''Dit is de Diskuschoonssiet vun en nich anmellt Bruker, de noch keen Brukerkonto anleggt hett oder dat jüst nich bruukt.\nWi mööt hier de numerische IP-Adress verwennen, üm den Bruker to identifizeern.\nSo en Adress kann vun verscheden Brukern bruukt warrn.\nWenn du en anonymen Bruker büst un meenst, dat disse Kommentaren nich an di richt sünd, denn [[Special:CreateAccount|legg di en Brukerkonto an]] oder [[Special:UserLogin|mell di an]], dat dat Problem nich mehr dor is.''",
-       "noarticletext": "Dor is opstunns keen Text op disse Sied. Du kannst [[Special:Search/{{PAGENAME}}|na dissen Utdruck in annere Sieden söken]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} in de Logböker söken],\noder [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse Sied ännern]</span>.",
+       "noarticletext": "Dor is opstunns keen Text op disse Siet. \nDu kannst [[Special:Search/{{PAGENAME}}|na dissen Utdruck in annere Sieden söken]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} in de Logböker söken], oder [{{fullurl:{{FULLPAGENAME}}|action=edit}} disse Siet ännern]</span>.",
        "noarticletext-nopermission": "Disse Sied hett opstunns keen Text.\nDu kannst in annere Sieden [[Special:Search/{{PAGENAME}}|na dissen Titel söken]]\noder <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} in de Logböker söken]</span>, man du hest nich dat Recht, de Sied optostellen.",
        "userpage-userdoesnotexist": "Dat Brukerkonto „<nowiki>$1</nowiki>“ gifft dat noch nich. Överlegg, wat du disse Siet würklich nee opstellen/ännern wullt.",
        "userpage-userdoesnotexist-view": "Dat Brukerkonto „$1“ gifft dat nich.",
        "session_fail_preview_html": "'''Deit uns leed! Wi kunnen dien Ännern nich spiekern, de Sitzungsdaten sünd verloren gahn.'''\n\n''In {{SITENAME}} is dat Spiekern vun rein HTML verlöövt, dorvun is de Vörschau utblennt, dat JavaScript-Angrepen nich mööglich sünd.''\n\n'''Versöök dat noch wedder un klick noch wedder op „Siet spiekern“. Wenn dat Problem noch jümmer dor is, [[Special:UserLogout|mell di af]] un denn wedder an.'''",
        "token_suffix_mismatch": "'''Dien Ännern sünd afwiest worrn. Dien Browser hett welk Teken in de Kuntrull-Tekenreeg kaputt maakt.\nWenn dat so spiekert warrt, kann dat angahn, dat noch mehr Teken in de Sied kaputt gaht.\nDat kann to’n Bispeel dor vun kamen, dat du en anonymen Proxy-Deenst bruukst, de wat verkehrt maakt.'''",
        "editing": "Ännern vun $1",
+       "creating": "Opstellen vun $1",
        "editingsection": "Ännern vun $1 (Afsatz)",
        "editingcomment": "Ännern vun $1 (nee Afsnidd)",
        "editconflict": "Konflikt bi’t Sied ännern: $1",
        "currentrev": "Aktuelle Version",
        "currentrev-asof": "Aktuelle Version vun’n $1",
        "revisionasof": "Version vun $1",
-       "revision-info": "Verschoon vun'n $4, Klock $5 vun $2",
+       "revision-info": "Verschoon vun'n $4, Klock $5 vun {{GENDER:$6|$2}}$7",
        "previousrevision": "Nächstöllere Version→",
        "nextrevision": "Ne’ere Version →",
        "currentrevisionlink": "aktuelle Version",
        "shown-title": "Wies $1 {{PLURAL:$1|Resultat|Resultaten}} per Sied",
        "viewprevnext": "Wies ($1 {{int:pipe-separator}} $2) ($3).",
        "searchmenu-exists": "* Sied '''[[$1]]'''",
-       "searchmenu-new": "'''Stell de Sied „[[:$1]]“ in dit Wiki nee op!'''",
+       "searchmenu-new": "<strong>Stell de Siet „[[:$1]]“ in dit Wiki nee op!</strong> {{PLURAL:$2|0=|Süh ok de Siet mit dien Sökresultat.|Süh ok de funnen Sökresultaten.}}",
        "searchprofile-articles": "Inholdsieden",
        "searchprofile-images": "Datein",
        "searchprofile-everything": "Allens",
        "powersearch-togglelabel": "Utwählen:",
        "powersearch-toggleall": "All",
        "powersearch-togglenone": "Keen",
+       "powersearch-remember": "Utwahl för latere sökanfragen marken",
        "search-external": "Externe Söök",
        "searchdisabled": "<p>De Vulltextsöök is wegen Överlast en Stoot deaktiveert. In disse Tied kannst du disse Google-Söök verwennen,\nde aver nich jümmer den aktuellsten Stand weerspegelt.<p>",
        "preferences": "Instellen",
        "action-userrights-interwiki": "de Rechten vun Brukers op annere Wikis to ännern",
        "action-siteadmin": "de Datenbank to sperren oder freetogeven",
        "nchanges": "{{PLURAL:$1|Een Ännern|$1 Ännern}}",
+       "enhancedrc-history": "Historie",
        "recentchanges": "Toletzt ännert",
        "recentchanges-legend": "Optionen för toletzt ännert",
        "recentchanges-summary": "Op disse Sied warrt de Sieden wiest, de toletzt ännert worrn sünd.",
        "recentchanges-label-minor": "Dat is en lütte Ännern",
        "recentchanges-label-bot": "Düsse Ännern worr maakt vun en Bot",
        "recentchanges-label-unpatrolled": "Düsse Ännern is noch nich kontrolleert worrn",
+       "recentchanges-label-plusminus": "Disse Siedengrött is mit dit Antall Bytes ännert",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (süh ok de [[Special:NewPages|List mit ne'e Sieden]])",
        "rcnotefrom": "Dit sünd de Ännern siet <b>$2</b> (bet to <b>$1</b> wiest).",
        "rclistfrom": "Wies ne’e Ännern siet $3 $2",
-       "rcshowhideminor": "$1 lütte Ännern",
-       "rcshowhidebots": "$1 Bots",
-       "rcshowhideliu": "$1 inloggte Brukers",
-       "rcshowhideanons": "$1 anonyme Brukers",
+       "rcshowhideminor": "lütte Ännern $1",
+       "rcshowhideminor-show": "wiesen",
+       "rcshowhideminor-hide": "versteken",
+       "rcshowhidebots": "Bots $1",
+       "rcshowhidebots-show": "wiesen",
+       "rcshowhidebots-hide": "versteken",
+       "rcshowhideliu": "registreerte Brukers $1",
+       "rcshowhideliu-show": "wiesen",
+       "rcshowhideliu-hide": "versteken",
+       "rcshowhideanons": "anonyme Brukers $1",
+       "rcshowhideanons-show": "wiesen",
+       "rcshowhideanons-hide": "versteken",
        "rcshowhidepatr": "$1 nakekene Ännern",
-       "rcshowhidemine": "$1 miene Ännern",
+       "rcshowhidepatr-show": "wiesen",
+       "rcshowhidepatr-hide": "versteken",
+       "rcshowhidemine": "miene Ännern $1",
+       "rcshowhidemine-show": "wiesen",
+       "rcshowhidemine-hide": "versteken",
+       "rcshowhidecategorization": "kategoriserung vun Sieden $1",
        "rclinks": "Wies de letzten '''$1''' Ännern vun de letzten '''$2''' Daag. ('''N''' - Ne’e Sieden; '''L''' - Lütte Ännern)<br />$3",
        "diff": "Ünnerscheed",
        "hist": "Versionen",
        "hide": "Nich wiesen",
-       "show": "Wiesen",
+       "show": "wiesen",
        "minoreditletter": "L",
        "newpageletter": "N",
        "boteditletter": "B",
        "pager-older-n": "{{PLURAL:$1|vörige|vörige $1}}",
        "suppress": "Oversight",
        "apisandbox-examples": "Bispeel",
-       "apisandbox-results": "Resultat",
+       "apisandbox-results": "Resultaten",
        "booksources": "Bookhannel",
        "booksources-search-legend": "Na Böker bi Bookhökers söken",
        "booksources-search": "Söken",
        "wlheader-showupdated": "Sieden, de siet dien letzten Besöök ännert worrn sünd, warrt '''fett''' wiest.",
        "wlnote": "Ünnen {{PLURAL:$1|steiht de letzte Ännern|staht de letzten $1 Ännern}} vun de {{PLURAL:$2|letzte Stünn|letzten '''$2''' Stünnen}}.",
        "wlshowlast": "Wies de letzten $1 Stünnen $2 Daag",
+       "watchlist-hide": "Versteken",
        "watchlist-options": "Optionen för de Oppasslist",
        "watching": "warrt op de Oppasslist ropsett...",
        "unwatching": "warrt vun de Oppasslist rünnernahmen...",
        "undelete-show-file-submit": "Jo",
        "namespace": "Naamruum:",
        "invert": "Utwahl ümkehren",
+       "namespace_association": "Tohörige Naamruum",
        "blanknamespace": "(Hööft-)",
-       "contributions": "Bidrääg vun den Bruker",
+       "contributions": "Bidrääg vun den {{GENDER:$1|Bruker}}",
        "contributions-title": "Brukerbidrääg vun „$1“",
        "mycontris": "Mien Arbeid",
        "contribsub2": "För $1 ($2)",
        "whatlinkshere-prev": "{{PLURAL:$1|vörige|vörige $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|nächste|nächste $1}}",
        "whatlinkshere-links": "← Lenken",
-       "whatlinkshere-hideredirs": "Redirects $1",
+       "whatlinkshere-hideredirs": "Redirects versteken",
        "whatlinkshere-hidetrans": "Vörlageninbinnungen $1",
-       "whatlinkshere-hidelinks": "Lenken $1",
+       "whatlinkshere-hidelinks": "Lenken versteken",
        "whatlinkshere-hideimages": "Dateilenken $1",
        "whatlinkshere-filters": "Filters",
        "autoblockid": "Autoblock #$1",
        "importlogpagetext": "Administrativen Import vun Sieden mit Versionsgeschicht vun annere Wikis.",
        "import-logentry-upload-detail": "{{PLURAL:$1|ene Version|$1 Versionen}}",
        "import-logentry-interwiki-detail": "{{PLURAL:$1|ene Version|$1 Versionen}} vun $2",
-       "tooltip-pt-userpage": "Dien Brukersied",
+       "tooltip-pt-userpage": "{{GENDER:|Dien}} Brukersiet",
        "tooltip-pt-anonuserpage": "De Brukersiet för de IP-Adress ünner de du schriffst",
-       "tooltip-pt-mytalk": "Dien Diskuschoonssied",
+       "tooltip-pt-mytalk": "{{GENDER:|Dien}} Diskuschoonssiet",
        "tooltip-pt-anontalk": "Diskuschoon över Ännern vun disse IP-Adress",
-       "tooltip-pt-preferences": "Mien Instellen",
+       "tooltip-pt-preferences": "{{GENDER:|Dien}} Instellen",
        "tooltip-pt-watchlist": "Mien Oppasslist",
-       "tooltip-pt-mycontris": "List vun dien Bidrääg",
+       "tooltip-pt-mycontris": "List vun {{GENDER:|dien}} Bidrääg",
        "tooltip-pt-login": "Du kannst di geern anmellen, dat is aver nich nödig, dat du Sieden ännern kannst.",
        "tooltip-pt-logout": "Afmellen",
        "tooltip-ca-talk": "Diskuschoon över disse Siet",
-       "tooltip-ca-edit": "Du kannst disse Siet ännern. Bruuk dat vör dat Spiekern.",
+       "tooltip-ca-edit": "Disse Siet ännern",
        "tooltip-ca-addsection": "En Afsnidd tofögen",
        "tooltip-ca-viewsource": "Disse Siet is schuult. Du kannst den Borntext ankieken.",
        "tooltip-ca-history": "Historie vun disse Siet.",
        "tooltip-t-recentchangeslinked": "Verlinkte Sieden",
        "tooltip-feed-rss": "RSS-Feed för disse Siet",
        "tooltip-feed-atom": "Atom-Feed för disse Siet",
-       "tooltip-t-contributions": "List vun de Bidreeg vun dissen Bruker",
+       "tooltip-t-contributions": "List vun de Bidrääg vun {{GENDER:$1|dissen Bruker}}",
        "tooltip-t-emailuser": "Dissen Bruker en E-Mail tostüren",
        "tooltip-t-upload": "Biller oder Mediendatein hoochladen",
        "tooltip-t-specialpages": "List vun alle Spezialsieden",
        "tooltip-ca-nstab-main": "Siet ankieken",
        "tooltip-ca-nstab-user": "Brukersiet ankieken",
        "tooltip-ca-nstab-media": "Mediensiet ankieken",
-       "tooltip-ca-nstab-special": "Dit is en Spezialsiet, du kannst disse Siet nich ännern.",
+       "tooltip-ca-nstab-special": "Dit is en Spezialsiet un kann nich ännert worrn.",
        "tooltip-ca-nstab-project": "Portalsiet ankieken",
        "tooltip-ca-nstab-image": "Bildsiet ankieken",
        "tooltip-ca-nstab-mediawiki": "Systemnarichten ankieken",
        "spambot_username": "MediaWiki Spam-Oprümen",
        "spam_reverting": "Trüchdreiht na de letzte Version ahn Lenken na $1.",
        "spam_blanking": "All Versionen harrn Lenken na $1, rein maakt.",
-       "simpleantispam-label": "Antispam-Kuntrull. Hier '''nix''' indragen!",
+       "simpleantispam-label": "Antispam-Kuntrull. \nHier '''nix''' indragen!",
        "pageinfo-title": "Informatschoon för \"$1\"",
        "pageinfo-article-id": "Sied-ID",
+       "pageinfo-toolboxlink": "Sieteninformatschonen",
        "pageinfo-redirectsto-info": "Info",
        "pageinfo-contentpage-yes": "Jo",
        "pageinfo-protect-cascading-yes": "Jo",
        "file-info-size": "$1 × $2 Pixel, Grött: $3, MIME-Typ: $4",
        "file-nohires": "Gifft dat Bild nich grötter.",
        "svg-long-desc": "SVG-Datei, Utgangsgrött: $1 × $2 Pixel, Dateigrött: $3",
-       "show-big-image": "Dat Bild wat grötter",
+       "show-big-image": "Originaldatei",
        "show-big-image-size": "$1 × $2 Pixels",
        "file-info-gif-looped": "löppt as Slööp",
        "file-info-gif-frames": "$1 {{PLURAL:$1|Bild|Biller}}",
        "htmlform-selectorother-other": "Annere",
        "sqlite-has-fts": "$1 mit Stöhn för Vulltext-Söök",
        "sqlite-no-fts": "$1 ahn Stöhn för Vulltext-Söök",
+       "logentry-delete-delete": "$1 {{GENDER:$2|wegsmeten}} Siet $3",
        "revdelete-restricted": "Inschränkungen för Administraters instellt",
        "revdelete-unrestricted": "Inschränkungen för Administraters rutnahmen",
        "logentry-block-block": "$1 {{GENDER:$2|block}} {{GENDER:$4|$3}} för en Tiedruum vun $5 $6",
index b00ef21..3b56b14 100644 (file)
        "log-action-filter-rights-rights": "Ręczna zmiana",
        "log-action-filter-rights-autopromote": "Automatyczna zmiana",
        "log-action-filter-upload-upload": "Nowe przesłane",
-       "log-action-filter-upload-overwrite": "Przesłane ponownie"
+       "log-action-filter-upload-overwrite": "Przesłane ponownie",
+       "authmanager-authplugin-setpass-denied": "Wtyczka uwierzytelniania nie zezwala na zmianę haseł.",
+       "authmanager-userdoesnotexist": "Konto użytkownika „$1” nie jest zarejestrowane.",
+       "authmanager-password-help": "Hasło do uwierzytelniania.",
+       "authmanager-email-label": "E-mail",
+       "authmanager-email-help": "Adres e‐mail",
+       "authmanager-realname-help": "Prawdziwe imię i nazwisko użytkownika",
+       "authprovider-resetpass-skip-label": "Pomiń",
+       "authform-notoken": "Brakujący token",
+       "authform-wrongtoken": "Nieprawidłowy token",
+       "specialpage-securitylevel-not-allowed": "Niestety, nie możesz korzystać z tej strony, ponieważ twoja tożsamość nie może zostać zweryfikowana.",
+       "authpage-cannot-login-continue": "Nie można kontynuować logowania. Sesja najprawdopodobniej wygasła.",
+       "cannotauth-not-allowed-title": "Brak dostępu",
+       "removecredentials-submit": "Usuń",
+       "removecredentials-submit-cancel": "Anuluj",
+       "credentialsform-account": "Nazwa konta:"
 }
index 7a6a253..6aff461 100644 (file)
        "authpage-cannot-create-continue": "Error message shown on authentication-related special pages when account creation cannot continue. This most likely means a session timeout.",
        "authpage-cannot-link": "Error message shown on authentication-related special pages when account linking cannot start. This is not supposed to happen unless the site is misconfigured.",
        "authpage-cannot-link-continue": "Error message shown on authentication-related special pages when account linking cannot continue. This most likely means a session timeout.",
-       "cannotauth-not-allowed-title": "Title of the error page shown when the user tries t use an authentication-related page they should not have access to.",
+       "cannotauth-not-allowed-title": "Title of the error page shown when the user tries to use an authentication-related page they should not have access to.",
        "cannotauth-not-allowed": "Text of the error page shown when the user tries t use an authentication-related page they should not have access to.",
        "changecredentials": "Title of the special page [[Special:ChangeCredentials]] which allows changing authentication credentials (such as the password).",
        "changecredentials-submit": "Used on [[Special:ChangeCredentials]].",
index 2e0be53..c9cd00b 100644 (file)
        "createacct-reason-ph": "Зачем вы создаёте другую учетную запись",
        "createacct-submit": "Создать учётную запись",
        "createacct-another-submit": "Создать учётную запись",
+       "createacct-continue-submit": "Продолжить создание учётной записи",
        "createacct-benefit-heading": "{{SITENAME}} — совместный труд таких же людей, как вы.",
        "createacct-benefit-body1": "{{PLURAL:$1|правка|правки|правок}}",
        "createacct-benefit-body2": "{{PLURAL:$1|статья|статьи|статей}}",
        "botpasswords-invalid-name": "Указанное имя участника не содержит разделителя для пароля бота («$1»).",
        "botpasswords-not-exist": "У участника «$1» нет пароля для бота с названием «$2».",
        "resetpass_forbidden": "Пароль не может быть изменён",
+       "resetpass_forbidden-reason": "Пароли не могут быть изменены: $1",
        "resetpass-no-info": "Чтобы обращаться непосредственно к этой странице, вам следует представиться системе.",
        "resetpass-submit-loggedin": "Изменить пароль",
        "resetpass-submit-cancel": "Отмена",
        "passwordreset-emailsentusername": "Если есть адрес электронной почты, связанный с этим именем участника, то будет отправлено письмо для восстановления пароля.",
        "passwordreset-emailsent-capture": "Отправлено электронное письмо с информацией о сбросе пароля, текст которого можно увидеть ниже.",
        "passwordreset-emailerror-capture": "Было создано электронное письмо с информацией о сбросе пароля, текст которого можно увидеть ниже, однако его не удалось отправить {{GENDER:$2|участнику|участнице}} по следующей причине: $1",
+       "passwordreset-invalideamil": "Недопустимый адрес электронной почты",
        "changeemail": "Изменить или удалить адрес электронной почты",
        "changeemail-header": "Заполните эту форму, чтобы изменить свой адрес электронной почты. Если вы хотите отвязать свой адрес электронной почты от учётной записи, то при заполнении формы оставьте пустым поле нового адреса электронной почты.",
        "changeemail-passwordrequired": "Чтобы подтвердить это изменение, вам нужно будет ввести свой пароль.",
        "accmailtext": "Сгенерированный случайным образом пароль для [[User talk:$1|$1]] выслан на адрес $2.\n\nПосле авторизации можно будет сменить пароль для этой учётной записи на ''[[Special:ChangePassword|специальной странице смены пароля]]''.",
        "newarticle": "(Новая)",
        "newarticletext": "Вы перешли по ссылке на страницу, которой пока не существует.\nЧтобы её создать, наберите текст в окне, расположенном ниже (подробнее см. [$1 справочную страницу]).\nЕсли вы оказались здесь по ошибке, просто нажмите кнопку '''назад''' своего браузера.",
-       "anontalkpagetext": "----''Эта страница обсуждения принадлежит анонимному участнику, который ещё не создал учётной записи, или не использует её.\nПоэтому для идентификации используется цифровой IP-адрес.\nЭтот же адрес может соответствовать нескольким другим участникам.\nЕсли вы анонимный участник и полагаете, что получили сообщения, адресованные не вам, пожалуйста, [[Special:CreateAccount|создайте учётную запись]] или [[Special:UserLogin|представьтесь системе]], чтобы впредь избежать возможной путаницы с другими анонимными участниками.''",
+       "anontalkpagetext": "----\n<em>Эта страница обсуждения анонимного участника, который ещё не создал учётной записи или не использует её.</em>\nПоэтому мы вынуждены для его/её идентификации использовать цифровой IP-адрес.\nЭтот же адрес может использоваться нескольким другим участникам.\nЕсли вы анонимный участник и полагаете, что получили сообщения, адресованные не вам, пожалуйста, [[Special:CreateAccount|создайте учётную запись]] или [[Special:UserLogin|представьтесь системе]], чтобы впредь избежать возможной путаницы с другими анонимными участниками.",
        "noarticletext": "В настоящий момент текст на данной странице отсутствует.\nВы можете [[Special:Search/{{PAGENAME}}|найти упоминание данного названия]] на других страницах,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} найти соответствующие записи журналов]\nили '''[{{fullurl:{{FULLPAGENAME}}|action=edit}} создать страницу с таким названием]'''</span>.",
        "noarticletext-nopermission": "В настоящее время на этой странице нет текста.\nВы можете [[Special:Search/{{PAGENAME}}|найти упоминание данного названия]] на других страницах,\nили <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} найти соответствующие записи журналов].</span> У вас нет разрешения создать данную страницу.",
        "missing-revision": "Версия $1 страницы «{{FULLPAGENAME}}» не существует.\n\nЭто обычно бывает, если последовать по устаревшей ссылке на страницу, которая была удалена.\nПодробности могут быть в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале удалений].",
        "log-action-filter-suppress-block": "Сокрытие пользователя через блокировки",
        "log-action-filter-suppress-reblock": "Сокрытие пользователя через повторное блокирование",
        "log-action-filter-upload-upload": "Новая загрузка",
-       "log-action-filter-upload-overwrite": "Повторно загрузить"
+       "log-action-filter-upload-overwrite": "Повторно загрузить",
+       "authmanager-authplugin-setpass-bad-domain": "Неверный домен.",
+       "authmanager-email-label": "Электронная почта",
+       "authmanager-email-help": "Адрес электронной почты",
+       "authmanager-realname-label": "Настоящее имя",
+       "authmanager-realname-help": "Настоящее имя участника",
+       "authmanager-provider-temporarypassword": "Временный пароль",
+       "authprovider-resetpass-skip-label": "Пропустить",
+       "authprovider-resetpass-skip-help": "Пропустить сброс пароля.",
+       "changecredentials-submit": "Изменить",
+       "changecredentials-submit-cancel": "Отмена",
+       "removecredentials-submit-cancel": "Отмена"
 }
index 5570761..e2adcf4 100644 (file)
@@ -10,6 +10,7 @@
        "tog-hideminor": "Fasal kayney kaŋ hun barmayyaŋ korawey ra tugu",
        "tog-hidepatrolled": "Fasalyan kurantey tugu barmay korawey ra",
        "tog-newpageshidepatrolled": "Moo kurantey tugu moo taaga maašeedaa ra",
+       "tog-hidecategorization": "Moɲey kanandiyanoo tugu",
        "tog-extendwatchlist": "Hawgay maašeedaa hayandi ka barmawey kul cebe, manti ikokorantaa hinne",
        "tog-usenewrc": "Barmawey marga moo bande barmay korawey nda hawgayhayey ra",
        "tog-numberheadings": "Boŋdekerey boŋkabuyan",
@@ -20,6 +21,7 @@
        "tog-watchdefault": "Moɲey nda tukey kaŋ ay g'i fasal tonton ay hawgayhayey ga",
        "tog-watchmoves": "Moɲey nda tukey kaŋ ay g'i ganandi tonton ay hawgayhayey ga",
        "tog-watchdeletion": "Moɲey nda tukey kaŋ ay g'i tuusu tonton ay hawgayhayey ga",
+       "tog-watchuploads": "Tuku taagey kaŋ g'i zijandi tonton hawgayhayey ga",
        "tog-watchrollback": "Moɲey kaŋ ay n'i taagandi tonton ay hawgayhayey ga",
        "tog-minordefault": "Fasalyaney kul šilbay sanda ikaynayaŋ nda tilasu",
        "tog-previewontop": "Moofuryan cebe jina fasal bataa ra",
@@ -31,8 +33,8 @@
        "tog-shownumberswatching": "Goykey kaŋ ga moɲoo hawgay hinnaa cebe",
        "tog-oldsig": "Kanbežeeri barantaa:",
        "tog-fancysig": "Kanbežeero tee sanda wikihantum (bila nda nga boŋdobu)",
-       "tog-uselivepreview": "Moofuryan goywaati ra (šiiyan)",
-       "tog-forceeditsummary": "Ay šaawar nda ya na fasal durandiyan dam",
+       "tog-uselivepreview": "Cebe kaŋ a ga dira",
+       "tog-forceeditsummary": "Ay šaawar nda fasal durandiyan koonu ga huru",
        "tog-watchlisthideown": "Ay boŋ fasalyaney tugu hawgayhayey ra",
        "tog-watchlisthidebots": "Maršin fasalyaney tugu hawgayhayey ra",
        "tog-watchlisthideminor": "Fasalyan kayney tugu hawgayhayey ra",
index fbe0e55..1e3c94d 100644 (file)
        "password-change-forbidden": "您不可變更此 Wiki 上的密碼。",
        "externaldberror": "這可能是由於資料庫驗證錯誤,或是不允許您更新外部帳號。",
        "login": "登入",
+       "login-security": "驗証您的 ID",
        "nav-login-createaccount": "登入/建立帳號",
        "userlogin": "登入/建立帳號",
        "userloginnocreate": "登入",
        "userlogin-resetpassword-link": "忘記密碼?",
        "userlogin-helplink2": "登入協助",
        "userlogin-loggedin": "您目前已登入 {{GENDER:$1|$1}} 使用者,\n請使用下列表單改登入另一位使用者。",
+       "userlogin-reauth": "您必須再登入一次來驗証您為 {{GENDER:$1|$1}}。",
        "userlogin-createanother": "建立另一個帳號",
        "createacct-emailrequired": "電子郵件地址",
        "createacct-emailoptional": "電子郵件地址 (選填)",
        "createaccountreason": "原因:",
        "createacct-reason": "原因",
        "createacct-reason-ph": "您為什麼要建立另一個帳號",
+       "createacct-reason-help": "顯示於帳號建立日誌的訊息",
        "createacct-submit": "建立您的帳號",
        "createacct-another-submit": "建立帳號",
+       "createacct-continue-submit": "繼續帳號建立",
+       "createacct-another-continue-submit": "繼續帳號建立",
        "createacct-benefit-heading": "{{SITENAME}} 是由像您一樣貢獻的人所建立的。",
        "createacct-benefit-body1": "{{PLURAL:$1|次編輯}}",
        "createacct-benefit-body2": "$1 頁",
        "nocookiesnew": "使用者帳號已建立成功,但您尚未登入。\n要登入 {{SITENAME}} 使用者需使用 Cookies,\n您的 Cookies 未尚開啟。\n請在開啟後使用您新的使用者名稱及密碼登入。",
        "nocookieslogin": "要登入 {{SITENAME}} 使用者需使用 Cookies,\n您的 Cookies 未尚開啟。\n請在開啟後重試。",
        "nocookiesfornew": "這個使用者的帳號未建立,我們不能確認它的來源。\n請確認您已開啟 Cookie,重新載入後再試。",
+       "createacct-loginerror": "已成功建立帳號,但無法自動登入。\n請繼續 [[Special:UserLogin|手動登入]]。",
        "noname": "您輸入的使用者名稱無效。",
        "loginsuccesstitle": "已登入",
        "loginsuccess": "<strong>{{GENDER:|您|妳|你}}現在已經以 \"$1\" 的身分登入了 {{SITENAME}}。</strong>",
-       "nosuchuser": "查無使用者 \"$1\"。\n使用者名稱有大小寫區分,\n請檢查您拼寫是否正確,或者 [[Special:CreateAccount|建立新帳號]]。",
+       "nosuchuser": "查無名稱為 \"$1\" 的使用者。\n使用者名稱有大小寫區分,\n請檢查您拼寫是否正確,或者 [[Special:CreateAccount|建立新帳號]]。",
        "nosuchusershort": "查無使用者 \"$1\",\n請檢查您拼寫是否正確。",
        "nouserspecified": "您必須指定一個使用者名稱。",
        "login-userblocked": "這位使用者已被封鎖,不允許登入。",
        "createacct-another-realname-tip": "真實姓名為選填欄位。\n若您提供真實姓名,它會用於使用者貢獻署名。",
        "pt-login": "登入",
        "pt-login-button": "登入",
+       "pt-login-continue-button": "繼續登入",
        "pt-createaccount": "建立帳號",
        "pt-userlogout": "登出",
        "php-mail-error-unknown": "PHP 的 mail() 函數發生不明錯誤。",
        "botpasswords-invalid-name": "指定的使用者名稱未包含機器人密碼分隔字元 (\"$1\")。",
        "botpasswords-not-exist": "使用者 \"$1\" 並沒有名稱為 \"$2\" 的機器人密碼。",
        "resetpass_forbidden": "無法變更密碼",
+       "resetpass_forbidden-reason": "無法變更密碼:$1",
        "resetpass-no-info": "您必須直接登入存取這個頁面。",
        "resetpass-submit-loggedin": "變更密碼",
        "resetpass-submit-cancel": "取消",
        "log-action-filter-suppress-block": "由封鎖禁止顯示使用者",
        "log-action-filter-suppress-reblock": "由重新封鎖禁止顯示使用者",
        "log-action-filter-upload-upload": "新上傳",
-       "log-action-filter-upload-overwrite": "重新上傳"
+       "log-action-filter-upload-overwrite": "重新上傳",
+       "authmanager-userdoesnotexist": "使用者帳號 \"$1\" 尚未註冊。",
+       "authmanager-username-help": "認証用的使用者名稱。",
+       "authmanager-password-help": "認証用的密碼。",
+       "authmanager-domain-help": "外部認証用的網域。",
+       "authmanager-retype-help": "再輸入一次密碼確認。",
+       "authmanager-email-label": "電子郵件",
+       "authmanager-email-help": "電子郵件地址",
+       "authmanager-realname-label": "真實姓名",
+       "authmanager-realname-help": "使用者的真實姓名",
+       "authmanager-provider-password": "Password-based 認証",
+       "authmanager-provider-password-domain": "Password- 及 domain-based 認証",
+       "authmanager-provider-temporarypassword": "臨時密碼",
+       "authprovider-resetpass-skip-label": "略過",
+       "authprovider-resetpass-skip-help": "略過重設密碼。",
+       "specialpage-securitylevel-not-allowed-title": "不允許",
+       "cannotauth-not-allowed-title": "權限不足",
+       "cannotauth-not-allowed": "您不被允許使用此頁面",
+       "changecredentials": "更改憑證",
+       "changecredentials-submit": "更改",
+       "changecredentials-submit-cancel": "取消",
+       "changecredentials-invalidsubpage": "$1 不是有效的憑証類型。",
+       "changecredentials-success": "已更改您的憑證。",
+       "removecredentials": "移除憑證",
+       "removecredentials-submit": "移除",
+       "removecredentials-submit-cancel": "取消",
+       "removecredentials-invalidsubpage": "$1 不是有效的憑証類型。",
+       "removecredentials-success": "已移除您的憑證。",
+       "credentialsform-provider": "憑證類型:",
+       "credentialsform-account": "帳號名稱:",
+       "cannotlink-no-provider-title": "沒有可連結的帳號",
+       "cannotlink-no-provider": "沒有可連結的帳號。",
+       "linkaccounts": "連結帳號",
+       "linkaccounts-success-text": "已連結帳號。",
+       "linkaccounts-submit": "連結帳號",
+       "unlinkaccounts": "取消連結帳號",
+       "unlinkaccounts-success": "已取消連結帳號。"
 }
index 9f3aa11..c242923 100644 (file)
@@ -221,6 +221,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $defaultOverrides->set( 'ObjectCaches', $objectCaches );
                $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
 
+               // Use a fast hash algorithm to hash passwords.
+               $defaultOverrides->set( 'PasswordDefault', 'A' );
+
                $testConfig = $customOverrides
                        ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
                        : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
index 6edf034..1bf8729 100644 (file)
@@ -15,8 +15,10 @@ class LinkerTest extends MediaWikiLangTestCase {
                        'wgArticlePath' => '/wiki/$1',
                ] );
 
-               $this->assertEquals( $expected,
-                       Linker::userLink( $userId, $userName, $altUserName, $msg )
+               $this->assertEquals(
+                       $expected,
+                       Linker::userLink( $userId, $userName, $altUserName ),
+                       $msg
                );
        }
 
index 51ef9d7..a45c3ae 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
+use MediaWiki\Interwiki\InterwikiLookup;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Services\ServiceDisabledException;
 
@@ -235,6 +236,7 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
                        'SiteStore' => [ 'SiteStore', SiteStore::class ],
                        'SiteLookup' => [ 'SiteLookup', SiteLookup::class ],
                        'StatsdDataFactory' => [ 'StatsdDataFactory', StatsdDataFactory::class ],
+                       'InterwikiLookup' => [ 'InterwikiLookup', InterwikiLookup::class ],
                        'EventRelayerGroup' => [ 'EventRelayerGroup', EventRelayerGroup::class ],
                        'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ],
                        'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ],
index 142c77f..13bfa46 100644 (file)
@@ -129,14 +129,16 @@ class TestUser {
                        throw new MWException( "Passed User has not been added to the database yet!" );
                }
 
+               if ( $user->checkPassword( $password ) === true ) {
+                       return;  // Nothing to do.
+               }
+
                $passwordFactory = new PasswordFactory();
                $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordFactory->setDefaultType( 'A' );
-               $pwhash = $passwordFactory->newFromPlaintext( $password );
+               $passwordHash = $passwordFactory->newFromPlaintext( $password );
                wfGetDB( DB_MASTER )->update(
                        'user',
-                       [ 'user_password' => $pwhash->toString() ],
+                       [ 'user_password' => $passwordHash->toString() ],
                        [ 'user_id' => $user->getId() ],
                        __METHOD__
                );
index f34af61..61b62aa 100644 (file)
@@ -106,7 +106,7 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
                );
        }
 
-       public function testUpdateAndResetNotificationTimestamp() {
+       public function testUpdateResetAndSetNotificationTimestamp() {
                $user = $this->getUser();
                $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
                $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
@@ -172,6 +172,24 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
                                [ [ $title, '20150202020202' ] ], $initialVisitingWatchers + 1
                        )
                );
+
+               // setNotificationTimestampsForUser specifying a title
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] )
+               );
+               $this->assertEquals(
+                       '20200202020202',
+                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
+               );
+
+               // setNotificationTimestampsForUser not specifying a title
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, '20210202020202' )
+               );
+               $this->assertEquals(
+                       '20210202020202',
+                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
+               );
        }
 
        public function testDuplicateAllAssociatedEntries() {
index 6c4a6f0..2d2e726 100644 (file)
@@ -2366,6 +2366,81 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                ScopedCallback::consume( $scopedOverrideRevision );
        }
 
+       public function testSetNotificationTimestampsForUser_anonUser() {
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $this->getMockDb() ),
+                       $this->getMockCache()
+               );
+               $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) );
+       }
+
+       public function testSetNotificationTimestampsForUser_allRows() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $timestamp = '20100101010101';
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
+                               [ 'wl_user' => 1 ]
+                       )
+                       ->will( $this->returnValue( true ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache()
+               );
+
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, $timestamp )
+               );
+       }
+
+       public function testSetNotificationTimestampsForUser_specificTargets() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $timestamp = '20100101010101';
+               $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
+                               [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
+                       )
+                       ->will( $this->returnValue( true ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'Foo' => 1, 'Bar' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache()
+               );
+
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
+               );
+       }
+
        public function testUpdateNotificationTimestamp_watchersExist() {
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
index 4a30292..75c5b50 100644 (file)
@@ -56,8 +56,6 @@ class ActionTest extends MediaWikiTestCase {
                        // Null and non-existing values
                        [ 'null', null ],
                        [ 'undeclared', null ],
-                       [ '', null ],
-                       [ false, null ],
                ];
        }
 
@@ -137,22 +135,39 @@ class ActionTest extends MediaWikiTestCase {
                $this->assertType( $expected ?: 'null', $action );
        }
 
-       public function testNull_doesNotExist() {
-               $exists = Action::exists( null );
+       public function emptyActionProvider() {
+               return [
+                       [ null ],
+                       [ false ],
+                       [ '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider emptyActionProvider
+        */
+       public function testEmptyAction_doesNotExist( $requestedAction ) {
+               $exists = Action::exists( $requestedAction );
 
                $this->assertFalse( $exists );
        }
 
-       public function testNull_defaultsToView() {
-               $context = $this->getContext( null );
+       /**
+        * @dataProvider emptyActionProvider
+        */
+       public function testEmptyAction_defaultsToView( $requestedAction ) {
+               $context = $this->getContext( $requestedAction );
                $actionName = Action::getActionName( $context );
 
                $this->assertEquals( 'view', $actionName );
        }
 
-       public function testNull_canNotBeInstantiated() {
+       /**
+        * @dataProvider emptyActionProvider
+        */
+       public function testEmptyAction_canNotBeInstantiated( $requestedAction ) {
                $page = $this->getPage();
-               $action = Action::factory( null, $page );
+               $action = Action::factory( $requestedAction, $page );
 
                $this->assertNull( $action );
        }
index e9afd45..2f8ffcc 100644 (file)
@@ -228,8 +228,7 @@ class ApiLoginTest extends ApiTestCase {
                $passwordFactory = new PasswordFactory();
                $passwordFactory->init( RequestContext::getMain()->getConfig() );
                // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordFactory->setDefaultType( 'A' );
-               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+               $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->insert(
@@ -237,7 +236,7 @@ class ApiLoginTest extends ApiTestCase {
                        [
                                'bp_user' => $centralId,
                                'bp_app_id' => 'foo',
-                               'bp_password' => $pwhash->toString(),
+                               'bp_password' => $passwordHash->toString(),
                                'bp_token' => '',
                                'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
                                'bp_grants' => '["test"]',
index 9c2bc75..c1449ea 100644 (file)
@@ -4,20 +4,18 @@ class MWDebugTest extends MediaWikiTestCase {
 
        protected function setUp() {
                parent::setUp();
-               // Make sure MWDebug class is enabled
-               static $MWDebugEnabled = false;
-               if ( !$MWDebugEnabled ) {
-                       MWDebug::init();
-                       $MWDebugEnabled = true;
-               }
                /** Clear log before each test */
                MWDebug::clearLog();
+       }
+
+       public static function setUpBeforeClass() {
+               MWDebug::init();
                MediaWiki\suppressWarnings();
        }
 
-       protected function tearDown() {
+       public static function tearDownAfterClass() {
+               MWDebug::deinit();
                MediaWiki\restoreWarnings();
-               parent::tearDown();
        }
 
        /**
diff --git a/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php b/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php
new file mode 100644 (file)
index 0000000..db6d002
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+/**
+ * @covers MediaWiki\Interwiki\ClassicInterwikiLookup
+ *
+ * @group MediaWiki
+ * @group Database
+ */
+class ClassicInterwikiLookupTest extends MediaWikiTestCase {
+
+       private function populateDB( $iwrows ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete( 'interwiki', '*', __METHOD__ );
+               $dbw->insert( 'interwiki', array_values( $iwrows ), __METHOD__ );
+               $this->tablesUsed[] = 'interwiki';
+       }
+
+       public function testDatabaseStorage() {
+               // NOTE: database setup is expensive, so we only do
+               //  it once and run all the tests in one go.
+               $dewiki = [
+                       'iw_prefix' => 'de',
+                       'iw_url' => 'http://de.wikipedia.org/wiki/',
+                       'iw_api' => 'http://de.wikipedia.org/w/api.php',
+                       'iw_wikiid' => 'dewiki',
+                       'iw_local' => 1,
+                       'iw_trans' => 0
+               ];
+
+               $zzwiki = [
+                       'iw_prefix' => 'zz',
+                       'iw_url' => 'http://zzwiki.org/wiki/',
+                       'iw_api' => 'http://zzwiki.org/w/api.php',
+                       'iw_wikiid' => 'zzwiki',
+                       'iw_local' => 0,
+                       'iw_trans' => 0
+               ];
+
+               $this->populateDB( [ $dewiki, $zzwiki ] );
+               $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup(
+                       Language::factory( 'en' ),
+                       WANObjectCache::newEmpty(),
+                       60*60,
+                       false,
+                       3,
+                       'en'
+               );
+
+               $this->assertEquals(
+                       [ $dewiki, $zzwiki ],
+                       $lookup->getAllPrefixes(),
+                       'getAllPrefixes()'
+               );
+               $this->assertEquals(
+                       [ $dewiki ],
+                       $lookup->getAllPrefixes( true ),
+                       'getAllPrefixes()'
+               );
+               $this->assertEquals(
+                       [ $zzwiki ],
+                       $lookup->getAllPrefixes( false ),
+                       'getAllPrefixes()'
+               );
+
+               $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' );
+               $this->assertFalse( $lookup->isValidInterwiki( 'xyz' ), 'unknown prefix is valid' );
+
+               $this->assertNull( $lookup->fetch( null ), 'no prefix' );
+               $this->assertFalse( $lookup->fetch( 'xyz' ), 'unknown prefix' );
+
+               $interwiki = $lookup->fetch( 'de' );
+               $this->assertInstanceOf( 'Interwiki', $interwiki );
+               $this->assertSame( $interwiki, $lookup->fetch( 'de' ), 'in-process caching' );
+
+               $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( 'http://de.wikipedia.org/w/api.php', $interwiki->getAPI(), 'getAPI' );
+               $this->assertSame( 'dewiki', $interwiki->getWikiID(), 'getWikiID' );
+               $this->assertSame( true, $interwiki->isLocal(), 'isLocal' );
+               $this->assertSame( false, $interwiki->isTranscludable(), 'isTranscludable' );
+
+               $lookup->invalidateCache( 'de' );
+               $this->assertNotSame( $interwiki, $lookup->fetch( 'de' ), 'invalidate cache' );
+       }
+
+       /**
+        * @param string $thisSite
+        * @param string[] $local
+        * @param string[] $global
+        *
+        * @return string[]
+        */
+       private function populateHash( $thisSite, $local, $global ) {
+               $hash = [];
+               $hash[ '__sites:' . wfWikiID() ] = $thisSite;
+
+               $globals = [];
+               $locals = [];
+
+               foreach ( $local as $row ) {
+                       $prefix = $row['iw_prefix'];
+                       $data = $row['iw_local'] . ' ' . $row['iw_url'];
+                       $locals[] = $prefix;
+                       $hash[ "_{$thisSite}:{$prefix}" ] = $data;
+               }
+
+               foreach ( $global as $row ) {
+                       $prefix = $row['iw_prefix'];
+                       $data = $row['iw_local'] . ' ' . $row['iw_url'];
+                       $globals[] = $prefix;
+                       $hash[ "__global:{$prefix}" ] = $data;
+               }
+
+               $hash[ '__list:__global' ] = implode( ' ', $globals );
+               $hash[ '__list:_' . $thisSite ] = implode( ' ', $locals );
+
+               return $hash;
+       }
+
+       private function populateCDB( $thisSite, $local, $global ) {
+               $cdbFile = tempnam( wfTempDir(), 'MW-ClassicInterwikiLookupTest-' ) . '.cdb';
+               $cdb = \Cdb\Writer::open( $cdbFile );
+
+               $hash = $this->populateHash( $thisSite, $local, $global );
+
+               foreach ( $hash as $key => $value ) {
+                       $cdb->set( $key, $value );
+               }
+
+               $cdb->close();
+               return $cdbFile;
+       }
+
+       public function testCDBStorage() {
+               // NOTE: CDB setup is expensive, so we only do
+               //  it once and run all the tests in one go.
+
+               $dewiki = [
+                       'iw_prefix' => 'de',
+                       'iw_url' => 'http://de.wikipedia.org/wiki/',
+                       'iw_local' => 1
+               ];
+
+               $zzwiki = [
+                       'iw_prefix' => 'zz',
+                       'iw_url' => 'http://zzwiki.org/wiki/',
+                       'iw_local' => 0
+               ];
+
+               $cdbFile = $this->populateCDB(
+                       'en',
+                       [ $dewiki ],
+                       [ $zzwiki ]
+               );
+               $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup(
+                       Language::factory( 'en' ),
+                       WANObjectCache::newEmpty(),
+                       60*60,
+                       $cdbFile,
+                       3,
+                       'en'
+               );
+
+               $this->assertEquals(
+                       [ $dewiki, $zzwiki ],
+                       $lookup->getAllPrefixes(),
+                       'getAllPrefixes()'
+               );
+
+               $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' );
+               $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' );
+
+               $interwiki = $lookup->fetch( 'de' );
+               $this->assertInstanceOf( 'Interwiki', $interwiki );
+
+               $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( true, $interwiki->isLocal(), 'isLocal' );
+
+               $interwiki = $lookup->fetch( 'zz' );
+               $this->assertInstanceOf( 'Interwiki', $interwiki );
+
+               $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( false, $interwiki->isLocal(), 'isLocal' );
+
+               // cleanup temp file
+               unlink( $cdbFile );
+       }
+
+       public function testArrayStorage() {
+               $dewiki = [
+                       'iw_prefix' => 'de',
+                       'iw_url' => 'http://de.wikipedia.org/wiki/',
+                       'iw_local' => 1
+               ];
+
+               $zzwiki = [
+                       'iw_prefix' => 'zz',
+                       'iw_url' => 'http://zzwiki.org/wiki/',
+                       'iw_local' => 0
+               ];
+
+               $hash = $this->populateHash(
+                       'en',
+                       [ $dewiki ],
+                       [ $zzwiki ]
+               );
+               $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup(
+                       Language::factory( 'en' ),
+                       WANObjectCache::newEmpty(),
+                       60*60,
+                       $hash,
+                       3,
+                       'en'
+               );
+
+               $this->assertEquals(
+                       [ $dewiki, $zzwiki ],
+                       $lookup->getAllPrefixes(),
+                       'getAllPrefixes()'
+               );
+
+               $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' );
+               $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' );
+
+               $interwiki = $lookup->fetch( 'de' );
+               $this->assertInstanceOf( 'Interwiki', $interwiki );
+
+               $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( true, $interwiki->isLocal(), 'isLocal' );
+
+               $interwiki = $lookup->fetch( 'zz' );
+               $this->assertInstanceOf( 'Interwiki', $interwiki );
+
+               $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( false, $interwiki->isLocal(), 'isLocal' );
+       }
+
+}
index 411d6a3..137dfb7 100644 (file)
@@ -1,4 +1,6 @@
 <?php
+use MediaWiki\MediaWikiServices;
+
 /**
  * @covers Interwiki
  *
@@ -47,7 +49,15 @@ class InterwikiTest extends MediaWikiTestCase {
                $this->tablesUsed[] = 'interwiki';
        }
 
+       private function setWgInterwikiCache( $interwikiCache ) {
+               $this->overrideMwServices();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' );
+               $this->setMwGlobals( 'wgInterwikiCache', $interwikiCache );
+       }
+
        public function testDatabaseStorage() {
+               $this->markTestSkipped( 'Needs I37b8e8018b3 <https://gerrit.wikimedia.org/r/#/c/270555/>' );
+
                // NOTE: database setup is expensive, so we only do
                //  it once and run all the tests in one go.
                $dewiki = [
@@ -70,8 +80,7 @@ class InterwikiTest extends MediaWikiTestCase {
 
                $this->populateDB( [ $dewiki, $zzwiki ] );
 
-               Interwiki::resetLocalCache();
-               $this->setMwGlobals( 'wgInterwikiCache', false );
+               $this->setWgInterwikiCache( false );
 
                $this->assertEquals(
                        [ $dewiki, $zzwiki ],
@@ -179,8 +188,7 @@ class InterwikiTest extends MediaWikiTestCase {
                        [ $zzwiki ]
                );
 
-               Interwiki::resetLocalCache();
-               $this->setMwGlobals( 'wgInterwikiCache', $cdbFile );
+               $this->setWgInterwikiCache( $cdbFile );
 
                $this->assertEquals(
                        [ $dewiki, $zzwiki ],
@@ -226,8 +234,7 @@ class InterwikiTest extends MediaWikiTestCase {
                        [ $zzwiki ]
                );
 
-               Interwiki::resetLocalCache();
-               $this->setMwGlobals( 'wgInterwikiCache', $cdbData );
+               $this->setWgInterwikiCache( $cdbData );
 
                $this->assertEquals(
                        [ $dewiki, $zzwiki ],
index edab0dc..d4b1587 100644 (file)
@@ -65,9 +65,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
        public function addDBDataOnce() {
                $passwordFactory = new \PasswordFactory();
                $passwordFactory->init( \RequestContext::getMain()->getConfig() );
-               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordFactory->setDefaultType( 'A' );
-               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+               $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
 
                $userId = \CentralIdLookup::factory( 'local' )->centralIdFromName( 'UTSysop' );
 
@@ -82,7 +80,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
                        [
                                'bp_user' => $userId,
                                'bp_app_id' => 'BotPasswordSessionProvider',
-                               'bp_password' => $pwhash->toString(),
+                               'bp_password' => $passwordHash->toString(),
                                'bp_token' => 'token!',
                                'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
                                'bp_grants' => '["test"]',
index 27ce287..629c6e5 100644 (file)
@@ -49,9 +49,7 @@ class BotPasswordTest extends MediaWikiTestCase {
        public function addDBData() {
                $passwordFactory = new \PasswordFactory();
                $passwordFactory->init( \RequestContext::getMain()->getConfig() );
-               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordFactory->setDefaultType( 'A' );
-               $pwhash = $passwordFactory->newFromPlaintext( 'foobaz' );
+               $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->delete(
@@ -65,7 +63,7 @@ class BotPasswordTest extends MediaWikiTestCase {
                                [
                                        'bp_user' => 42,
                                        'bp_app_id' => 'BotPassword',
-                                       'bp_password' => $pwhash->toString(),
+                                       'bp_password' => $passwordHash->toString(),
                                        'bp_token' => 'token!',
                                        'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
                                        'bp_grants' => '["test"]',
@@ -73,7 +71,7 @@ class BotPasswordTest extends MediaWikiTestCase {
                                [
                                        'bp_user' => 43,
                                        'bp_app_id' => 'BotPassword',
-                                       'bp_password' => $pwhash->toString(),
+                                       'bp_password' => $passwordHash->toString(),
                                        'bp_token' => 'token!',
                                        'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
                                        'bp_grants' => '["test"]',
@@ -311,8 +309,6 @@ class BotPasswordTest extends MediaWikiTestCase {
        public function testSave( $password ) {
                $passwordFactory = new \PasswordFactory();
                $passwordFactory->init( \RequestContext::getMain()->getConfig() );
-               // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordFactory->setDefaultType( 'A' );
 
                $bp = BotPassword::newUnsaved( [
                        'centralId' => 42,
@@ -325,9 +321,9 @@ class BotPasswordTest extends MediaWikiTestCase {
                        BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check'
                );
 
-               $pwhash = $password ? $passwordFactory->newFromPlaintext( $password ) : null;
-               $this->assertFalse( $bp->save( 'update', $pwhash ) );
-               $this->assertTrue( $bp->save( 'insert', $pwhash ) );
+               $passwordHash = $password ? $passwordFactory->newFromPlaintext( $password ) : null;
+               $this->assertFalse( $bp->save( 'update', $passwordHash ) );
+               $this->assertTrue( $bp->save( 'insert', $passwordHash ) );
                $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
                $this->assertInstanceOf( 'BotPassword', $bp2 );
                $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
@@ -356,9 +352,9 @@ class BotPasswordTest extends MediaWikiTestCase {
                        $this->assertTrue( $pw->equals( $password ) );
                }
 
-               $pwhash = $passwordFactory->newFromPlaintext( 'XXX' );
+               $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' );
                $token = $bp->getToken();
-               $this->assertTrue( $bp->save( 'update', $pwhash ) );
+               $this->assertTrue( $bp->save( 'update', $passwordHash ) );
                $this->assertNotEquals( $token, $bp->getToken() );
                $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
                $this->assertTrue( $pw->equals( 'XXX' ) );