Merge "RCFilters: rephrase the feedback link text"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 30 Aug 2017 18:00:38 +0000 (18:00 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 30 Aug 2017 18:00:38 +0000 (18:00 +0000)
96 files changed:
RELEASE-NOTES-1.30
autoload.php
includes/Block.php
includes/CommentStore.php [new file with mode: 0644]
includes/CommentStoreComment.php [new file with mode: 0644]
includes/DefaultSettings.php
includes/EditPage.php
includes/FeedUtils.php
includes/Revision.php
includes/Title.php
includes/WatchedItemQueryService.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryBlocks.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryProtectedTitles.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryUsers.php
includes/api/ApiQueryWatchlist.php
includes/api/i18n/es.json
includes/api/i18n/eu.json
includes/changes/ChangesListBooleanFilter.php
includes/changes/RecentChange.php
includes/export/WikiExporter.php
includes/export/XmlDumpWriter.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/import/WikiImporter.php
includes/import/WikiRevision.php
includes/installer/DatabaseUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/installer/i18n/es.json
includes/logging/LogEntry.php
includes/logging/LogPage.php
includes/page/PageArchive.php
includes/page/WikiPage.php
includes/rcfeed/IRCColourfulRCFeedFormatter.php
includes/revisiondelete/RevDelLogItem.php
includes/revisiondelete/RevDelLogList.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialWatchlist.php
includes/specials/pagers/BlockListPager.php
includes/specials/pagers/DeletedContribsPager.php
includes/specials/pagers/ImageListPager.php
includes/specials/pagers/NewPagesPager.php
includes/specials/pagers/ProtectedPagesPager.php
languages/i18n/ais.json [new file with mode: 0644]
languages/i18n/be-tarask.json
languages/i18n/ce.json
languages/i18n/es.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gu.json
languages/i18n/hr.json
languages/i18n/hsb.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/lt.json
languages/i18n/ne.json
languages/i18n/nn.json
languages/i18n/or.json
languages/i18n/qqq.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/su.json
maintenance/archives/patch-comment-table.sql [new file with mode: 0644]
maintenance/migrateComments.php [new file with mode: 0644]
maintenance/orphans.php
maintenance/postgres/archives/patch-comment-table.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/rebuildrecentchanges.php
maintenance/sqlite/archives/patch-comment-table.sql [new file with mode: 0644]
maintenance/tables.sql
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.toolbar/images/ksh/LICENSE
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/CommentStoreTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionStorageTest.php
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/changes/RecentChangeTest.php
tests/phpunit/includes/changes/TestRecentChangesHelper.php
tests/phpunit/includes/deferred/LinksUpdateTest.php
tests/phpunit/includes/logging/LogFormatterTestCase.php
tests/phpunit/includes/page/WikiPageTest.php
tests/phpunit/suites/ParserTestTopLevelSuite.php

index c7270ee..c4c56e8 100644 (file)
@@ -61,6 +61,11 @@ section).
   just been unwatched.
 * Added $wgParserTestMediaHandlers, where mock media handlers can be passed to
   MediaHandlerFactory for parser tests.
+* Edit summaries, block reasons, and other "comments" are now stored in a
+  separate database table. Use the CommentFormatter class to access them.
+** This is currently gated by $wgCommentTableSchemaMigrationStage. Most wikis
+   can set this to MIGRATION_NEW and run maintenance/migrateComments.php as
+   soon as any necessary extensions are updated.
 
 === External library changes in 1.30 ===
 
index 8062387..3a2ae10 100644 (file)
@@ -276,6 +276,8 @@ $wgAutoloadLocalClasses = [
        'CollationFa' => __DIR__ . '/includes/collation/CollationFa.php',
        'CommandLineInc' => __DIR__ . '/maintenance/commandLine.inc',
        'CommandLineInstaller' => __DIR__ . '/maintenance/install.php',
+       'CommentStore' => __DIR__ . '/includes/CommentStore.php',
+       'CommentStoreComment' => __DIR__ . '/includes/CommentStoreComment.php',
        'CompareParserCache' => __DIR__ . '/maintenance/compareParserCache.php',
        'CompareParsers' => __DIR__ . '/maintenance/compareParsers.php',
        'ComposerHookHandler' => __DIR__ . '/includes/composer/ComposerHookHandler.php',
@@ -982,6 +984,7 @@ $wgAutoloadLocalClasses = [
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
        'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php',
        'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php',
+       'MigrateComments' => __DIR__ . '/maintenance/migrateComments.php',
        'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php',
        'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php',
        'MimeAnalyzer' => __DIR__ . '/includes/libs/mime/MimeAnalyzer.php',
index 05e97b9..0b17e93 100644 (file)
@@ -199,6 +199,8 @@ class Block {
        /**
         * Return the list of ipblocks fields that should be selected to create
         * a new block.
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        public static function selectFields() {
@@ -207,7 +209,6 @@ class Block {
                        'ipb_address',
                        'ipb_by',
                        'ipb_by_text',
-                       'ipb_reason',
                        'ipb_timestamp',
                        'ipb_auto',
                        'ipb_anon_only',
@@ -218,7 +219,7 @@ class Block {
                        'ipb_block_email',
                        'ipb_allow_usertalk',
                        'ipb_parent_block_id',
-               ];
+               ] + CommentStore::newKey( 'ipb_reason' )->getFields();
        }
 
        /**
@@ -411,7 +412,6 @@ class Block {
                        $this->setBlocker( $row->ipb_by_text );
                }
 
-               $this->mReason = $row->ipb_reason;
                $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
                $this->mAuto = $row->ipb_auto;
                $this->mHideName = $row->ipb_deleted;
@@ -419,7 +419,11 @@ class Block {
                $this->mParentBlockId = $row->ipb_parent_block_id;
 
                // I wish I didn't have to do this
-               $this->mExpiry = wfGetDB( DB_REPLICA )->decodeExpiry( $row->ipb_expiry );
+               $db = wfGetDB( DB_REPLICA );
+               $this->mExpiry = $db->decodeExpiry( $row->ipb_expiry );
+               $this->mReason = CommentStore::newKey( 'ipb_reason' )
+                       // Legacy because $row probably came from self::selectFields()
+                       ->getCommentLegacy( $db, $row )->text;
 
                $this->isHardblock( !$row->ipb_anon_only );
                $this->isAutoblocking( $row->ipb_enable_autoblock );
@@ -488,7 +492,7 @@ class Block {
                        self::purgeExpired();
                }
 
-               $row = $this->getDatabaseArray();
+               $row = $this->getDatabaseArray( $dbw );
                $row['ipb_id'] = $dbw->nextSequenceValue( "ipblocks_ipb_id_seq" );
 
                $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
@@ -558,7 +562,7 @@ class Block {
                        // update corresponding autoblock(s) (T50813)
                        $dbw->update(
                                'ipblocks',
-                               $this->getAutoblockUpdateArray(),
+                               $this->getAutoblockUpdateArray( $dbw ),
                                [ 'ipb_parent_block_id' => $this->getId() ],
                                __METHOD__
                        );
@@ -583,14 +587,11 @@ class Block {
 
        /**
         * Get an array suitable for passing to $dbw->insert() or $dbw->update()
-        * @param IDatabase $db
+        * @param IDatabase $dbw
         * @return array
         */
-       protected function getDatabaseArray( $db = null ) {
-               if ( !$db ) {
-                       $db = wfGetDB( DB_REPLICA );
-               }
-               $expiry = $db->encodeExpiry( $this->mExpiry );
+       protected function getDatabaseArray( IDatabase $dbw ) {
+               $expiry = $dbw->encodeExpiry( $this->mExpiry );
 
                if ( $this->forcedTargetID ) {
                        $uid = $this->forcedTargetID;
@@ -603,8 +604,7 @@ class Block {
                        'ipb_user'             => $uid,
                        'ipb_by'               => $this->getBy(),
                        'ipb_by_text'          => $this->getByName(),
-                       'ipb_reason'           => $this->mReason,
-                       'ipb_timestamp'        => $db->timestamp( $this->mTimestamp ),
+                       'ipb_timestamp'        => $dbw->timestamp( $this->mTimestamp ),
                        'ipb_auto'             => $this->mAuto,
                        'ipb_anon_only'        => !$this->isHardblock(),
                        'ipb_create_account'   => $this->prevents( 'createaccount' ),
@@ -616,23 +616,23 @@ class Block {
                        'ipb_block_email'      => $this->prevents( 'sendemail' ),
                        'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
                        'ipb_parent_block_id'  => $this->mParentBlockId
-               ];
+               ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
 
                return $a;
        }
 
        /**
+        * @param IDatabase $dbw
         * @return array
         */
-       protected function getAutoblockUpdateArray() {
+       protected function getAutoblockUpdateArray( IDatabase $dbw ) {
                return [
                        'ipb_by'               => $this->getBy(),
                        'ipb_by_text'          => $this->getByName(),
-                       'ipb_reason'           => $this->mReason,
                        'ipb_create_account'   => $this->prevents( 'createaccount' ),
                        'ipb_deleted'          => (int)$this->mHideName, // typecast required for SQLite
                        'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
-               ];
+               ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
        }
 
        /**
diff --git a/includes/CommentStore.php b/includes/CommentStore.php
new file mode 100644 (file)
index 0000000..0c86c1e
--- /dev/null
@@ -0,0 +1,567 @@
+<?php
+/**
+ * Manage storage of comments in the database
+ *
+ * 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 Wikimedia\Rdbms\IDatabase;
+
+/**
+ * CommentStore handles storage of comments (edit summaries, log reasons, etc)
+ * in the database.
+ * @since 1.30
+ */
+class CommentStore {
+
+       /**
+        * Define fields that use temporary tables for transitional purposes
+        * @var array Keys are '$key', values are arrays with four fields:
+        *  - table: Temporary table name
+        *  - pk: Temporary table column referring to the main table's primary key
+        *  - field: Temporary table column referring comment.comment_id
+        *  - joinPK: Main table's primary key
+        */
+       protected static $tempTables = [
+               'rev_comment' => [
+                       'table' => 'revision_comment_temp',
+                       'pk' => 'revcomment_rev',
+                       'field' => 'revcomment_comment_id',
+                       'joinPK' => 'rev_id',
+               ],
+               'img_description' => [
+                       'table' => 'image_comment_temp',
+                       'pk' => 'imgcomment_name',
+                       'field' => 'imgcomment_description_id',
+                       'joinPK' => 'img_name',
+               ],
+       ];
+
+       /**
+        * Fields that formerly used $tempTables
+        * @var array Key is '$key', value is the MediaWiki version in which it was
+        *  removed from $tempTables.
+        */
+       protected static $formerTempTables = [];
+
+       /** @var string */
+       protected $key;
+
+       /** @var int One of the MIGRATION_* constants */
+       protected $stage;
+
+       /** @var array|null Cache for `self::getJoin()` */
+       protected $joinCache = null;
+
+       /**
+        * @param string $key A key such as "rev_comment" identifying the comment
+        *  field being fetched.
+        */
+       public function __construct( $key ) {
+               global $wgCommentTableSchemaMigrationStage;
+
+               $this->key = $key;
+               $this->stage = $wgCommentTableSchemaMigrationStage;
+       }
+
+       /**
+        * Static constructor for easier chaining
+        * @param string $key A key such as "rev_comment" identifying the comment
+        *  field being fetched.
+        * @return CommentStore
+        */
+       public static function newKey( $key ) {
+               return new CommentStore( $key );
+       }
+
+       /**
+        * Get SELECT fields for the comment key
+        *
+        * Each resulting row should be passed to `self::getCommentLegacy()` to get the
+        * actual comment.
+        *
+        * @note Use of this method may require a subsequent database query to
+        *  actually fetch the comment. If possible, use `self::getJoin()` instead.
+        * @return string[] to include in the `$vars` to `IDatabase->select()`. All
+        *  fields are aliased, so `+` is safe to use.
+        */
+       public function getFields() {
+               $fields = [];
+               if ( $this->stage === MIGRATION_OLD ) {
+                       $fields["{$this->key}_text"] = $this->key;
+                       $fields["{$this->key}_data"] = 'NULL';
+                       $fields["{$this->key}_cid"] = 'NULL';
+               } else {
+                       if ( $this->stage < MIGRATION_NEW ) {
+                               $fields["{$this->key}_old"] = $this->key;
+                       }
+                       if ( isset( self::$tempTables[$this->key] ) ) {
+                               $fields["{$this->key}_pk"] = self::$tempTables[$this->key]['joinPK'];
+                       } else {
+                               $fields["{$this->key}_id"] = "{$this->key}_id";
+                       }
+               }
+               return $fields;
+       }
+
+       /**
+        * Get SELECT fields and joins for the comment key
+        *
+        * Each resulting row should be passed to `self::getComment()` to get the
+        * actual comment.
+        *
+        * @return array With three keys:
+        *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+        *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        *  All tables, fields, and joins are aliased, so `+` is safe to use.
+        */
+       public function getJoin() {
+               if ( $this->joinCache === null ) {
+                       $tables = [];
+                       $fields = [];
+                       $joins = [];
+
+                       if ( $this->stage === MIGRATION_OLD ) {
+                               $fields["{$this->key}_text"] = $this->key;
+                               $fields["{$this->key}_data"] = 'NULL';
+                               $fields["{$this->key}_cid"] = 'NULL';
+                       } else {
+                               $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN';
+
+                               if ( isset( self::$tempTables[$this->key] ) ) {
+                                       $t = self::$tempTables[$this->key];
+                                       $alias = "temp_$this->key";
+                                       $tables[$alias] = $t['table'];
+                                       $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
+                                       $joinField = "{$alias}.{$t['field']}";
+                               } else {
+                                       $joinField = "{$this->key}_id";
+                               }
+
+                               $alias = "comment_$this->key";
+                               $tables[$alias] = 'comment';
+                               $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
+
+                               if ( $this->stage === MIGRATION_NEW ) {
+                                       $fields["{$this->key}_text"] = "{$alias}.comment_text";
+                               } else {
+                                       $fields["{$this->key}_text"] = "COALESCE( {$alias}.comment_text, $this->key )";
+                               }
+                               $fields["{$this->key}_data"] = "{$alias}.comment_data";
+                               $fields["{$this->key}_cid"] = "{$alias}.comment_id";
+                       }
+
+                       $this->joinCache = [
+                               'tables' => $tables,
+                               'fields' => $fields,
+                               'joins' => $joins,
+                       ];
+               }
+
+               return $this->joinCache;
+       }
+
+       /**
+        * Extract the comment from a row
+        *
+        * Shared implementation for getComment() and getCommentLegacy()
+        *
+        * @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment()
+        * @param object|array $row
+        * @param bool $fallback
+        * @return CommentStoreComment
+        */
+       private function getCommentInternal( IDatabase $db = null, $row, $fallback = false ) {
+               $key = $this->key;
+               $row = (array)$row;
+               if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
+                       $cid = isset( $row["{$key}_cid"] ) ? $row["{$key}_cid"] : null;
+                       $text = $row["{$key}_text"];
+                       $data = $row["{$key}_data"];
+               } elseif ( $this->stage === MIGRATION_OLD ) {
+                       $cid = null;
+                       if ( $fallback && isset( $row[$key] ) ) {
+                               wfLogWarning( "Using deprecated fallback handling for comment $key" );
+                               $text = $row[$key];
+                       } else {
+                               wfLogWarning( "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD" );
+                               $text = '';
+                       }
+                       $data = null;
+               } else {
+                       if ( isset( self::$tempTables[$key] ) ) {
+                               if ( array_key_exists( "{$key}_pk", $row ) ) {
+                                       if ( !$db ) {
+                                               throw new InvalidArgumentException(
+                                                       "\$row does not contain fields needed for comment $key and getComment(), but "
+                                                       . "does have fields for getCommentLegacy()"
+                                               );
+                                       }
+                                       $t = self::$tempTables[$key];
+                                       $id = $row["{$key}_pk"];
+                                       $row2 = $db->selectRow(
+                                               [ $t['table'], 'comment' ],
+                                               [ 'comment_id', 'comment_text', 'comment_data' ],
+                                               [ $t['pk'] => $id ],
+                                               __METHOD__,
+                                               [],
+                                               [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
+                                       );
+                               } elseif ( $fallback && isset( $row[$key] ) ) {
+                                       wfLogWarning( "Using deprecated fallback handling for comment $key" );
+                                       $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
+                               } else {
+                                       throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
+                               }
+                       } else {
+                               if ( array_key_exists( "{$key}_id", $row ) ) {
+                                       if ( !$db ) {
+                                               throw new InvalidArgumentException(
+                                                       "\$row does not contain fields needed for comment $key and getComment(), but "
+                                                       . "does have fields for getCommentLegacy()"
+                                               );
+                                       }
+                                       $id = $row["{$key}_id"];
+                                       $row2 = $db->selectRow(
+                                               'comment',
+                                               [ 'comment_id', 'comment_text', 'comment_data' ],
+                                               [ 'comment_id' => $id ],
+                                               __METHOD__
+                                       );
+                               } elseif ( $fallback && isset( $row[$key] ) ) {
+                                       wfLogWarning( "Using deprecated fallback handling for comment $key" );
+                                       $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
+                               } else {
+                                       throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
+                               }
+                       }
+
+                       if ( $row2 ) {
+                               $cid = $row2->comment_id;
+                               $text = $row2->comment_text;
+                               $data = $row2->comment_data;
+                       } elseif ( $this->stage < MIGRATION_NEW && array_key_exists( "{$key}_old", $row ) ) {
+                               $cid = null;
+                               $text = $row["{$key}_old"];
+                               $data = null;
+                       } else {
+                               // @codeCoverageIgnoreStart
+                               wfLogWarning( "Missing comment row for $key, id=$id" );
+                               $cid = null;
+                               $text = '';
+                               $data = null;
+                               // @codeCoverageIgnoreEnd
+                       }
+               }
+
+               $msg = null;
+               if ( $data !== null ) {
+                       $data = FormatJson::decode( $data );
+                       if ( !is_object( $data ) ) {
+                               // @codeCoverageIgnoreStart
+                               wfLogWarning( "Invalid JSON object in comment: $data" );
+                               $data = null;
+                               // @codeCoverageIgnoreEnd
+                       } else {
+                               $data = (array)$data;
+                               if ( isset( $data['_message'] ) ) {
+                                       $msg = self::decodeMessage( $data['_message'] )
+                                               ->setInterfaceMessageFlag( true );
+                               }
+                               if ( !empty( $data['_null'] ) ) {
+                                       $data = null;
+                               } else {
+                                       foreach ( $data as $k => $v ) {
+                                               if ( substr( $k, 0, 1 ) === '_' ) {
+                                                       unset( $data[$k] );
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return new CommentStoreComment( $cid, $text, $msg, $data );
+       }
+
+       /**
+        * Extract the comment from a row
+        *
+        * Use `self::getJoin()` to ensure the row contains the needed data.
+        *
+        * If you need to fake a comment in a row for some reason, set fields
+        * `{$key}_text` (string) and `{$key}_data` (JSON string or null).
+        *
+        * @param object|array $row Result row.
+        * @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
+        * @return CommentStoreComment
+        */
+       public function getComment( $row, $fallback = false ) {
+               return $this->getCommentInternal( null, $row, $fallback );
+       }
+
+       /**
+        * Extract the comment from a row, with legacy lookups.
+        *
+        * If `$row` might have been generated using `self::getFields()` rather
+        * than `self::getJoin()`, use this. Prefer `self::getComment()` if you
+        * know callers used `self::getJoin()` for the row fetch.
+        *
+        * If you need to fake a comment in a row for some reason, set fields
+        * `{$key}_text` (string) and `{$key}_data` (JSON string or null).
+        *
+        * @param IDatabase $db Database handle to use for lookup
+        * @param object|array $row Result row.
+        * @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
+        * @return CommentStoreComment
+        */
+       public function getCommentLegacy( IDatabase $db, $row, $fallback = false ) {
+               return $this->getCommentInternal( $db, $row, $fallback );
+       }
+
+       /**
+        * Create a new CommentStoreComment, inserting it into the database if necessary
+        *
+        * If a comment is going to be passed to `self::insert()` or the like
+        * multiple times, it will be more efficient to pass a CommentStoreComment
+        * once rather than making `self::insert()` do it every time through.
+        *
+        * @note When passing a CommentStoreComment, this may set `$comment->id` if
+        *  it's not already set. If `$comment->id` is already set, it will not be
+        *  verified that the specified comment actually exists or that it
+        *  corresponds to the comment text, message, and/or data in the
+        *  CommentStoreComment.
+        * @param IDatabase $dbw Database handle to insert on. Unused if `$comment`
+        *  is a CommentStoreComment and `$comment->id` is set.
+        * @param string|Message|CommentStoreComment $comment Comment text or Message object, or
+        *  a CommentStoreComment.
+        * @param array|null $data Structured data to store. Keys beginning with '_' are reserved.
+        *  Ignored if $comment is a CommentStoreComment.
+        * @return CommentStoreComment
+        */
+       public function createComment( IDatabase $dbw, $comment, array $data = null ) {
+               global $wgContLang;
+
+               if ( !$comment instanceof CommentStoreComment ) {
+                       if ( $data !== null ) {
+                               foreach ( $data as $k => $v ) {
+                                       if ( substr( $k, 0, 1 ) === '_' ) {
+                                               throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' );
+                                       }
+                               }
+                       }
+                       if ( $comment instanceof Message ) {
+                               $message = clone $comment;
+                               $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg
+                                       ->setInterfaceMessageFlag( true )
+                                       ->text();
+                               $comment = new CommentStoreComment( null, $text, $message, $data );
+                       } else {
+                               $comment = new CommentStoreComment( null, $comment, null, $data );
+                       }
+               }
+
+               if ( $this->stage > MIGRATION_OLD && !$comment->id ) {
+                       $dbData = $comment->data;
+                       if ( !$comment->message instanceof RawMessage ) {
+                               if ( $dbData === null ) {
+                                       $dbData = [ '_null' => true ];
+                               }
+                               $dbData['_message'] = self::encodeMessage( $comment->message );
+                       }
+                       if ( $dbData !== null ) {
+                               $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
+                       }
+
+                       $hash = self::hash( $comment->text, $dbData );
+                       $comment->id = $dbw->selectField(
+                               'comment',
+                               'comment_id',
+                               [
+                                       'comment_hash' => $hash,
+                                       'comment_text' => $comment->text,
+                                       'comment_data' => $dbData,
+                               ],
+                               __METHOD__
+                       );
+                       if ( !$comment->id ) {
+                               $comment->id = $dbw->nextSequenceValue( 'comment_comment_id_seq' );
+                               $dbw->insert(
+                                       'comment',
+                                       [
+                                               'comment_id' => $comment->id,
+                                               'comment_hash' => $hash,
+                                               'comment_text' => $comment->text,
+                                               'comment_data' => $dbData,
+                                       ],
+                                       __METHOD__
+                               );
+                               $comment->id = $dbw->insertId();
+                       }
+               }
+
+               return $comment;
+       }
+
+       /**
+        * Implementation for `self::insert()` and `self::insertWithTempTable()`
+        * @param IDatabase $dbw
+        * @param string|Message|CommentStoreComment $comment
+        * @param array|null $data
+        * @return array [ array $fields, callable $callback ]
+        */
+       private function insertInternal( IDatabase $dbw, $comment, $data ) {
+               $fields = [];
+               $callback = null;
+
+               $comment = $this->createComment( $dbw, $comment, $data );
+
+               if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+                       $fields[$this->key] = $comment->text;
+               }
+
+               if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+                       if ( isset( self::$tempTables[$this->key] ) ) {
+                               $t = self::$tempTables[$this->key];
+                               $func = __METHOD__;
+                               $commentId = $comment->id;
+                               $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
+                                       $dbw->insert(
+                                               $t['table'],
+                                               [
+                                                       $t['pk'] => $id,
+                                                       $t['field'] => $commentId,
+                                               ],
+                                               $func
+                                       );
+                               };
+                       } else {
+                               $fields["{$this->key}_id"] = $comment->id;
+                       }
+               }
+
+               return [ $fields, $callback ];
+       }
+
+       /**
+        * Prepare for the insertion of a row with a comment
+        *
+        * @note It's recommended to include both the call to this method and the
+        *  row insert in the same transaction.
+        * @param IDatabase $dbw Database handle to insert on
+        * @param string|Message|CommentStoreComment $comment As for `self::createComment()`
+        * @param array|null $data As for `self::createComment()`
+        * @return array Fields for the insert or update
+        */
+       public function insert( IDatabase $dbw, $comment, $data = null ) {
+               if ( isset( self::$tempTables[$this->key] ) ) {
+                       throw new InvalidArgumentException( "Must use insertWithTempTable() for $this->key" );
+               }
+
+               list( $fields ) = $this->insertInternal( $dbw, $comment, $data );
+               return $fields;
+       }
+
+       /**
+        * Prepare for the insertion of a row with a comment and temporary table
+        *
+        * This is currently needed for "rev_comment" and "img_description". In the
+        * future that requirement will be removed.
+        *
+        * @note It's recommended to include both the call to this method and the
+        *  row insert in the same transaction.
+        * @param IDatabase $dbw Database handle to insert on
+        * @param string|Message|CommentStoreComment $comment As for `self::createComment()`
+        * @param array|null $data As for `self::createComment()`
+        * @return array Two values:
+        *  - array Fields for the insert or update
+        *  - callable Function to call when the primary key of the row being
+        *    inserted/updated is known. Pass it that primary key.
+        */
+       public function insertWithTempTable( IDatabase $dbw, $comment, $data = null ) {
+               if ( isset( self::$formerTempTables[$this->key] ) ) {
+                       wfDeprecated( __METHOD__ . " for $this->key", self::$formerTempTables[$this->key] );
+               } elseif ( !isset( self::$tempTables[$this->key] ) ) {
+                       throw new InvalidArgumentException( "Must use insert() for $this->key" );
+               }
+
+               list( $fields, $callback ) = $this->insertInternal( $dbw, $comment, $data );
+               if ( !$callback ) {
+                       $callback = function () {
+                               // Do nothing.
+                       };
+               }
+               return [ $fields, $callback ];
+       }
+
+       /**
+        * Encode a Message as a PHP data structure
+        * @param Message $msg
+        * @return array
+        */
+       protected static function encodeMessage( Message $msg ) {
+               $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
+               $params = $msg->getParams();
+               foreach ( $params as &$param ) {
+                       if ( $param instanceof Message ) {
+                               $param = [
+                                       'message' => self::encodeMessage( $param )
+                               ];
+                       }
+               }
+               array_unshift( $params, $key );
+               return $params;
+       }
+
+       /**
+        * Decode a message that was encoded by self::encodeMessage()
+        * @param array $data
+        * @return Message
+        */
+       protected static function decodeMessage( $data ) {
+               $key = array_shift( $data );
+               foreach ( $data as &$param ) {
+                       if ( is_object( $param ) ) {
+                               $param = (array)$param;
+                       }
+                       if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
+                               $param = self::decodeMessage( $param['message'] );
+                       }
+               }
+               return new Message( $key, $data );
+       }
+
+       /**
+        * Hashing function for comment storage
+        * @param string $text Comment text
+        * @param string|null $data Comment data
+        * @return int 32-bit signed integer
+        */
+       public static function hash( $text, $data ) {
+               $hash = crc32( $text ) ^ crc32( (string)$data );
+
+               // 64-bit PHP returns an unsigned CRC, change it to signed for
+               // insertion into the database.
+               if ( $hash >= 0x80000000 ) {
+                       $hash |= -1 << 32;
+               }
+
+               return $hash;
+       }
+
+}
diff --git a/includes/CommentStoreComment.php b/includes/CommentStoreComment.php
new file mode 100644 (file)
index 0000000..afc1374
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Value object for CommentStore
+ *
+ * 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 Wikimedia\Rdbms\IDatabase;
+
+/**
+ * CommentStoreComment represents a comment stored by CommentStore. The fields
+ * should be considered read-only.
+ * @since 1.30
+ */
+class CommentStoreComment {
+
+       /** @var int|null Comment ID, if any */
+       public $id;
+
+       /** @var string Text version of the comment */
+       public $text;
+
+       /** @var Message Message version of the comment. Might be a RawMessage */
+       public $message;
+
+       /** @var array|null Structured data of the comment */
+       public $data;
+
+       /**
+        * @private For use by CommentStore only
+        * @param int|null $id
+        * @param string $text
+        * @param Message|null $message
+        * @param array|null $data
+        */
+       public function __construct( $id, $text, Message $message = null, array $data = null ) {
+               $this->id = $id;
+               $this->text = $text;
+               $this->message = $message ?: new RawMessage( '$1', [ $text ] );
+               $this->data = $data;
+       }
+}
index 44461b3..cf3e569 100644 (file)
@@ -8763,6 +8763,13 @@ $wgExperiencedUserMemberSince = 30; # days
  */
 $wgInterwikiPrefixDisplayTypes = [];
 
+/**
+ * Comment table schema migration stage.
+ * @since 1.30
+ * @var int One of the MIGRATION_* constants
+ */
+$wgCommentTableSchemaMigrationStage = MIGRATION_OLD;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 72a072d..40913bb 100644 (file)
@@ -2701,7 +2701,7 @@ class EditPage {
 
                if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
                        $username = $this->lastDelete->user_name;
-                       $comment = $this->lastDelete->log_comment;
+                       $comment = CommentStore::newKey( 'log_comment' )->getComment( $this->lastDelete )->text;
 
                        // It is better to not parse the comment at all than to have templates expanded in the middle
                        // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
@@ -3687,8 +3687,9 @@ class EditPage {
         */
        protected function getLastDelete() {
                $dbr = wfGetDB( DB_REPLICA );
+               $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
                $data = $dbr->selectRow(
-                       [ 'logging', 'user' ],
+                       [ 'logging', 'user' ] + $commentQuery['tables'],
                        [
                                'log_type',
                                'log_action',
@@ -3696,11 +3697,10 @@ class EditPage {
                                'log_user',
                                'log_namespace',
                                'log_title',
-                               'log_comment',
                                'log_params',
                                'log_deleted',
                                'user_name'
-                       ], [
+                       ] + $commentQuery['fields'], [
                                'log_namespace' => $this->mTitle->getNamespace(),
                                'log_title' => $this->mTitle->getDBkey(),
                                'log_type' => 'delete',
@@ -3708,7 +3708,10 @@ class EditPage {
                                'user_id=log_user'
                        ],
                        __METHOD__,
-                       [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
+                       [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
+                       [
+                               'user' => [ 'JOIN', 'user_id=log_user' ],
+                       ] + $commentQuery['joins']
                );
                // Quick paranoid permission checks...
                if ( is_object( $data ) ) {
@@ -3717,7 +3720,8 @@ class EditPage {
                        }
 
                        if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
-                               $data->log_comment = $this->context->msg( 'rev-deleted-comment' )->escaped();
+                               $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
+                               $data->log_comment_data = null;
                        }
                }
 
index 96a88d3..0def6a0 100644 (file)
@@ -72,7 +72,8 @@ class FeedUtils {
        /**
         * Format a diff for the newsfeed
         *
-        * @param object $row Row from the recentchanges table
+        * @param object $row Row from the recentchanges table, including fields as
+        *  appropriate for CommentStore
         * @return string
         */
        public static function formatDiff( $row ) {
@@ -88,7 +89,9 @@ class FeedUtils {
                        $timestamp,
                        $row->rc_deleted & Revision::DELETED_COMMENT
                                ? wfMessage( 'rev-deleted-comment' )->escaped()
-                               : $row->rc_comment,
+                               : CommentStore::newKey( 'rc_comment' )
+                                       // Legacy from RecentChange::selectFields() via ChangesListSpecialPage::doMainQuery()
+                                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text,
                        $actiontext
                );
        }
index e457beb..ff4a284 100644 (file)
@@ -192,7 +192,9 @@ class Revision implements IDBAccessObject {
                $attribs = $overrides + [
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
-                       'comment'    => $row->ar_comment,
+                       'comment'    => CommentStore::newKey( 'ar_comment' )
+                               // Legacy because $row probably came from self::selectArchiveFields()
+                               ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
                        'user'       => $row->ar_user,
                        'user_text'  => $row->ar_user_text,
                        'timestamp'  => $row->ar_timestamp,
@@ -443,6 +445,8 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision.
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        public static function selectFields() {
@@ -453,7 +457,6 @@ class Revision implements IDBAccessObject {
                        'rev_page',
                        'rev_text_id',
                        'rev_timestamp',
-                       'rev_comment',
                        'rev_user_text',
                        'rev_user',
                        'rev_minor_edit',
@@ -463,6 +466,8 @@ class Revision implements IDBAccessObject {
                        'rev_sha1',
                ];
 
+               $fields += CommentStore::newKey( 'rev_comment' )->getFields();
+
                if ( $wgContentHandlerUseDB ) {
                        $fields[] = 'rev_content_format';
                        $fields[] = 'rev_content_model';
@@ -474,6 +479,8 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision from an archive row.
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        public static function selectArchiveFields() {
@@ -485,7 +492,6 @@ class Revision implements IDBAccessObject {
                        'ar_text',
                        'ar_text_id',
                        'ar_timestamp',
-                       'ar_comment',
                        'ar_user_text',
                        'ar_user',
                        'ar_minor_edit',
@@ -495,6 +501,8 @@ class Revision implements IDBAccessObject {
                        'ar_sha1',
                ];
 
+               $fields += CommentStore::newKey( 'ar_comment' )->getFields();
+
                if ( $wgContentHandlerUseDB ) {
                        $fields[] = 'ar_content_format';
                        $fields[] = 'ar_content_model';
@@ -568,7 +576,9 @@ class Revision implements IDBAccessObject {
                        $this->mId = intval( $row->rev_id );
                        $this->mPage = intval( $row->rev_page );
                        $this->mTextId = intval( $row->rev_text_id );
-                       $this->mComment = $row->rev_comment;
+                       $this->mComment = CommentStore::newKey( 'rev_comment' )
+                               // Legacy because $row probably came from self::selectFields()
+                               ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
                        $this->mUser = intval( $row->rev_user );
                        $this->mMinorEdit = intval( $row->rev_minor_edit );
                        $this->mTimestamp = $row->rev_timestamp;
@@ -1455,7 +1465,6 @@ class Revision implements IDBAccessObject {
                        'rev_id'         => $rev_id,
                        'rev_page'       => $this->mPage,
                        'rev_text_id'    => $this->mTextId,
-                       'rev_comment'    => $this->mComment,
                        'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
                        'rev_user'       => $this->mUser,
                        'rev_user_text'  => $this->mUserText,
@@ -1470,6 +1479,10 @@ class Revision implements IDBAccessObject {
                                : $this->mSha1,
                ];
 
+               list( $commentFields, $commentCallback ) =
+                       CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
+               $row += $commentFields;
+
                if ( $wgContentHandlerUseDB ) {
                        // NOTE: Store null for the default model and format, to save space.
                        // XXX: Makes the DB sensitive to changed defaults.
@@ -1498,6 +1511,7 @@ class Revision implements IDBAccessObject {
                        // Only if nextSequenceValue() was called
                        $this->mId = $dbw->insertId();
                }
+               $commentCallback( $this->mId );
 
                // Assertion to try to catch T92046
                if ( (int)$this->mId === 0 ) {
index 729628e..0687a15 100644 (file)
@@ -2670,24 +2670,33 @@ class Title implements LinkTarget {
 
                if ( $this->mTitleProtection === null ) {
                        $dbr = wfGetDB( DB_REPLICA );
+                       $commentStore = new CommentStore( 'pt_reason' );
+                       $commentQuery = $commentStore->getJoin();
                        $res = $dbr->select(
-                               'protected_titles',
+                               [ 'protected_titles' ] + $commentQuery['tables'],
                                [
                                        'user' => 'pt_user',
-                                       'reason' => 'pt_reason',
                                        'expiry' => 'pt_expiry',
                                        'permission' => 'pt_create_perm'
-                               ],
+                               ] + $commentQuery['fields'],
                                [ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
-                               __METHOD__
+                               __METHOD__,
+                               [],
+                               $commentQuery['joins']
                        );
 
                        // fetchRow returns false if there are no rows.
                        $row = $dbr->fetchRow( $res );
                        if ( $row ) {
-                               $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
+                               $this->mTitleProtection = [
+                                       'user' => $row['user'],
+                                       'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
+                                       'permission' => $row['permission'],
+                                       'reason' => $commentStore->getComment( $row )->text,
+                               ];
+                       } else {
+                               $this->mTitleProtection = false;
                        }
-                       $this->mTitleProtection = $row;
                }
                return $this->mTitleProtection;
        }
index 1fafb24..d0f45be 100644 (file)
@@ -55,6 +55,10 @@ class WatchedItemQueryService {
        /** @var WatchedItemQueryServiceExtension[]|null */
        private $extensions = null;
 
+       /**
+        * @var CommentStore|null */
+       private $commentStore = null;
+
        public function __construct( LoadBalancer $loadBalancer ) {
                $this->loadBalancer = $loadBalancer;
        }
@@ -78,6 +82,13 @@ class WatchedItemQueryService {
                return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
        }
 
+       private function getCommentStore() {
+               if ( !$this->commentStore ) {
+                       $this->commentStore = new CommentStore( 'rc_comment' );
+               }
+               return $this->commentStore;
+       }
+
        /**
         * @param User $user
         * @param array $options Allowed keys:
@@ -172,13 +183,9 @@ class WatchedItemQueryService {
                        );
                }
 
-               $tables = [ 'recentchanges', 'watchlist' ];
-               if ( !$options['allRevisions'] ) {
-                       $tables[] = 'page';
-               }
-
                $db = $this->getConnection();
 
+               $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
                $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
                $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
                $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
@@ -320,6 +327,17 @@ class WatchedItemQueryService {
                return array_intersect_key( $allFields, array_flip( $rcKeys ) );
        }
 
+       private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
+               $tables = [ 'recentchanges', 'watchlist' ];
+               if ( !$options['allRevisions'] ) {
+                       $tables[] = 'page';
+               }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $tables += $this->getCommentStore()->getJoin()['tables'];
+               }
+               return $tables;
+       }
+
        private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
                $fields = [
                        'rc_id',
@@ -355,7 +373,7 @@ class WatchedItemQueryService {
                        $fields[] = 'rc_user';
                }
                if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
-                       $fields[] = 'rc_comment';
+                       $fields += $this->getCommentStore()->getJoin()['fields'];
                }
                if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
                        $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
@@ -657,6 +675,9 @@ class WatchedItemQueryService {
                if ( !$options['allRevisions'] ) {
                        $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
                }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $joinConds += $this->getCommentStore()->getJoin()['joins'];
+               }
                return $joinConds;
        }
 
index fd95e17..d594ad4 100644 (file)
@@ -49,6 +49,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' );
 
                $db = $this->getDB();
+               $commentStore = new CommentStore( 'ipb_reason' );
 
                $prop = $params['prop'];
                if ( !is_null( $prop ) ) {
@@ -263,7 +264,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                $data['blockedby'] = $row->ipb_by_text;
                                $data['blockedbyid'] = (int)$row->ipb_by;
                                $data['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
-                               $data['blockreason'] = $row->ipb_reason;
+                               $data['blockreason'] = $commentStore->getComment( $row )->text;
                                $data['blockexpiry'] = $row->ipb_expiry;
                        }
                        if ( $row->ipb_deleted ) {
index 44526e8..fe16134 100644 (file)
@@ -456,10 +456,13 @@ abstract class ApiQueryBase extends ApiBase {
                                'ipb_id',
                                'ipb_by',
                                'ipb_by_text',
-                               'ipb_reason',
                                'ipb_expiry',
                                'ipb_timestamp'
                        ] );
+                       $commentQuery = CommentStore::newKey( 'ipb_reason' )->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
                }
 
                // Don't show hidden names
index 076a09e..698c13c 100644 (file)
@@ -37,6 +37,7 @@ class ApiQueryBlocks extends ApiQueryBase {
 
        public function execute() {
                $db = $this->getDB();
+               $commentStore = new CommentStore( 'ipb_reason' );
                $params = $this->extractRequestParams();
                $this->requireMaxOneParameter( $params, 'users', 'ip' );
 
@@ -61,12 +62,18 @@ class ApiQueryBlocks extends ApiQueryBase {
                $this->addFieldsIf( 'ipb_by_text', $fld_by );
                $this->addFieldsIf( 'ipb_by', $fld_byid );
                $this->addFieldsIf( 'ipb_expiry', $fld_expiry );
-               $this->addFieldsIf( 'ipb_reason', $fld_reason );
                $this->addFieldsIf( [ 'ipb_range_start', 'ipb_range_end' ], $fld_range );
                $this->addFieldsIf( [ 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock',
                        'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk' ],
                        $fld_flags );
 
+               if ( $fld_reason ) {
+                       $commentQuery = $commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
                $this->addTimestampWhereRange(
                        'ipb_timestamp',
@@ -205,7 +212,7 @@ class ApiQueryBlocks extends ApiQueryBase {
                                $block['expiry'] = ApiResult::formatExpiry( $row->ipb_expiry );
                        }
                        if ( $fld_reason ) {
-                               $block['reason'] = $row->ipb_reason;
+                               $block['reason'] = $commentStore->getComment( $row )->text;
                        }
                        if ( $fld_range && !$row->ipb_auto ) {
                                $block['rangestart'] = IP::formatHex( $row->ipb_range_start );
index b68a868..5dd007b 100644 (file)
@@ -44,6 +44,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
 
                $user = $this->getUser();
                $db = $this->getDB();
+               $commentStore = new CommentStore( 'ar_comment' );
                $params = $this->extractRequestParams( false );
                $prop = array_flip( $params['prop'] );
                $fld_parentid = isset( $prop['parentid'] );
@@ -115,11 +116,17 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                $this->addFieldsIf( 'ar_rev_id', $fld_revid );
                $this->addFieldsIf( 'ar_user_text', $fld_user );
                $this->addFieldsIf( 'ar_user', $fld_userid );
-               $this->addFieldsIf( 'ar_comment', $fld_comment || $fld_parsedcomment );
                $this->addFieldsIf( 'ar_minor_edit', $fld_minor );
                $this->addFieldsIf( 'ar_len', $fld_len );
                $this->addFieldsIf( 'ar_sha1', $fld_sha1 );
 
+               if ( $fld_comment || $fld_parsedcomment ) {
+                       $commentQuery = $commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                if ( $fld_tags ) {
                        $this->addTables( 'tag_summary' );
                        $this->addJoinConds(
@@ -322,12 +329,13 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                        $anyHidden = true;
                                }
                                if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_COMMENT, $user ) ) {
+                                       $comment = $commentStore->getComment( $row )->text;
                                        if ( $fld_comment ) {
-                                               $rev['comment'] = $row->ar_comment;
+                                               $rev['comment'] = $comment;
                                        }
                                        if ( $fld_parsedcomment ) {
                                                $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
-                                               $rev['parsedcomment'] = Linker::formatComment( $row->ar_comment, $title );
+                                               $rev['parsedcomment'] = Linker::formatComment( $comment, $title );
                                        }
                                }
                        }
index 7383cba..212b613 100644 (file)
@@ -43,6 +43,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
 
                $user = $this->getUser();
                $db = $this->getDB();
+               $commentStore = new CommentStore( 'fa_description' );
 
                $params = $this->extractRequestParams();
 
@@ -66,13 +67,19 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $this->addFieldsIf( 'fa_sha1', $fld_sha1 );
                $this->addFieldsIf( [ 'fa_user', 'fa_user_text' ], $fld_user );
                $this->addFieldsIf( [ 'fa_height', 'fa_width', 'fa_size' ], $fld_dimensions || $fld_size );
-               $this->addFieldsIf( 'fa_description', $fld_description );
                $this->addFieldsIf( [ 'fa_major_mime', 'fa_minor_mime' ], $fld_mime );
                $this->addFieldsIf( 'fa_media_type', $fld_mediatype );
                $this->addFieldsIf( 'fa_metadata', $fld_metadata );
                $this->addFieldsIf( 'fa_bits', $fld_bitdepth );
                $this->addFieldsIf( 'fa_archive_name', $fld_archivename );
 
+               if ( $fld_description ) {
+                       $commentQuery = $commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                if ( !is_null( $params['continue'] ) ) {
                        $cont = explode( '|', $params['continue'] );
                        $this->dieContinueUsageIf( count( $cont ) != 3 );
@@ -165,10 +172,10 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        if ( $fld_description &&
                                Revision::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user )
                        ) {
-                               $file['description'] = $row->fa_description;
+                               $file['description'] = $commentStore->getComment( $row )->text;
                                if ( isset( $prop['parseddescription'] ) ) {
                                        $file['parseddescription'] = Linker::formatComment(
-                                               $row->fa_description, $title );
+                                               $file['description'], $title );
                                }
                        }
                        if ( $fld_user &&
index 3e8bccc..3066720 100644 (file)
@@ -31,6 +31,8 @@
  */
 class ApiQueryLogEvents extends ApiQueryBase {
 
+       private $commentStore;
+
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'le' );
        }
@@ -43,6 +45,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
        public function execute() {
                $params = $this->extractRequestParams();
                $db = $this->getDB();
+               $this->commentStore = new CommentStore( 'log_comment' );
                $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
 
                $prop = array_flip( $params['prop'] );
@@ -91,9 +94,15 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        [ 'log_namespace', 'log_title' ],
                        $this->fld_title || $this->fld_parsedcomment
                );
-               $this->addFieldsIf( 'log_comment', $this->fld_comment || $this->fld_parsedcomment );
                $this->addFieldsIf( 'log_params', $this->fld_details );
 
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       $commentQuery = $this->commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                if ( $this->fld_tags ) {
                        $this->addTables( 'tag_summary' );
                        $this->addJoinConds( [ 'tag_summary' => [ 'LEFT JOIN', 'log_id=ts_log_id' ] ] );
@@ -327,18 +336,19 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
                }
 
-               if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->log_comment ) ) {
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = true;
                                $anyHidden = true;
                        }
                        if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
+                               $comment = $this->commentStore->getComment( $row )->text;
                                if ( $this->fld_comment ) {
-                                       $vals['comment'] = $row->log_comment;
+                                       $vals['comment'] = $comment;
                                }
 
                                if ( $this->fld_parsedcomment ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $row->log_comment, $title );
+                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
                                }
                        }
                }
index 5f6510e..b69a299 100644 (file)
@@ -55,10 +55,17 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
 
                $prop = array_flip( $params['prop'] );
                $this->addFieldsIf( 'pt_user', isset( $prop['user'] ) || isset( $prop['userid'] ) );
-               $this->addFieldsIf( 'pt_reason', isset( $prop['comment'] ) || isset( $prop['parsedcomment'] ) );
                $this->addFieldsIf( 'pt_expiry', isset( $prop['expiry'] ) );
                $this->addFieldsIf( 'pt_create_perm', isset( $prop['level'] ) );
 
+               if ( isset( $prop['comment'] ) || isset( $prop['parsedcomment'] ) ) {
+                       $commentStore = new CommentStore( 'pt_reason' );
+                       $commentQuery = $commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                $this->addTimestampWhereRange( 'pt_timestamp', $params['dir'], $params['start'], $params['end'] );
                $this->addWhereFld( 'pt_namespace', $params['namespace'] );
                $this->addWhereFld( 'pt_create_perm', $params['level'] );
@@ -127,11 +134,13 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
                                }
 
                                if ( isset( $prop['comment'] ) ) {
-                                       $vals['comment'] = $row->pt_reason;
+                                       $vals['comment'] = $commentStore->getComment( $row )->text;
                                }
 
                                if ( isset( $prop['parsedcomment'] ) ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $row->pt_reason, $title );
+                                       $vals['parsedcomment'] = Linker::formatComment(
+                                               $commentStore->getComment( $row )->text, $titles
+                                       );
                                }
 
                                if ( isset( $prop['expiry'] ) ) {
index 0dc01aa..9af4e3e 100644 (file)
@@ -36,6 +36,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                parent::__construct( $query, $moduleName, 'rc' );
        }
 
+       private $commentStore;
+
        private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
                $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false,
                $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false,
@@ -274,7 +276,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 
                        /* Add fields to our query if they are specified as a needed parameter. */
                        $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
-                       $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
                        $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
                        $this->addFieldsIf( 'rc_user_text', $this->fld_user );
                        $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
@@ -286,6 +287,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        );
                        $showRedirects = $this->fld_redirect || isset( $show['redirect'] )
                                || isset( $show['!redirect'] );
+
+                       if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                               $this->commentStore = new CommentStore( 'rc_comment' );
+                               $commentQuery = $this->commentStore->getJoin();
+                               $this->addTables( $commentQuery['tables'] );
+                               $this->addFields( $commentQuery['fields'] );
+                               $this->addJoinConds( $commentQuery['joins'] );
+                       }
                }
                $this->addFieldsIf( [ 'rc_this_oldid' ],
                        $resultPageSet && $params['generaterevisions'] );
@@ -500,12 +509,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                                $anyHidden = true;
                        }
                        if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
-                               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
-                                       $vals['comment'] = $row->rc_comment;
+                               $comment = $this->commentStore->getComment( $row )->text;
+                               if ( $this->fld_comment ) {
+                                       $vals['comment'] = $comment;
                                }
 
-                               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+                               if ( $this->fld_parsedcomment ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
                                }
                        }
                }
index 181cddb..bb0f335 100644 (file)
@@ -36,7 +36,7 @@ class ApiQueryContributions extends ApiQueryBase {
        }
 
        private $params, $prefixMode, $userprefix, $multiUserMode, $idMode, $usernames, $userids,
-               $parentLens;
+               $parentLens, $commentStore;
        private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
                $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
                $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
@@ -45,6 +45,8 @@ class ApiQueryContributions extends ApiQueryBase {
                // Parse some parameters
                $this->params = $this->extractRequestParams();
 
+               $this->commentStore = new CommentStore( 'rev_comment' );
+
                $prop = array_flip( $this->params['prop'] );
                $this->fld_ids = isset( $prop['ids'] );
                $this->fld_title = isset( $prop['title'] );
@@ -341,12 +343,18 @@ class ApiQueryContributions extends ApiQueryBase {
                $this->addFieldsIf( 'rev_page', $this->fld_ids );
                $this->addFieldsIf( 'page_latest', $this->fld_flags );
                // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
-               $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
                $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
                $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
                $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
                $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
 
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       $commentQuery = $this->commentStore->getJoin();
+                       $this->addTables( $commentQuery['tables'] );
+                       $this->addFields( $commentQuery['fields'] );
+                       $this->addJoinConds( $commentQuery['joins'] );
+               }
+
                if ( $this->fld_tags ) {
                        $this->addTables( 'tag_summary' );
                        $this->addJoinConds(
@@ -416,7 +424,7 @@ class ApiQueryContributions extends ApiQueryBase {
                        $vals['top'] = $row->page_latest == $row->rev_id;
                }
 
-               if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) {
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
                                $vals['commenthidden'] = true;
                                $anyHidden = true;
@@ -428,12 +436,13 @@ class ApiQueryContributions extends ApiQueryBase {
                        );
 
                        if ( $userCanView ) {
+                               $comment = $this->commentStore->getComment( $row )->text;
                                if ( $this->fld_comment ) {
-                                       $vals['comment'] = $row->rev_comment;
+                                       $vals['comment'] = $comment;
                                }
 
                                if ( $this->fld_parsedcomment ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $row->rev_comment, $title );
+                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
                                }
                        }
                }
index 2a0eadd..fbf1f9e 100644 (file)
@@ -99,6 +99,7 @@ class ApiQueryUsers extends ApiQueryBase {
 
        public function execute() {
                $db = $this->getDB();
+               $commentStore = new CommentStore( 'ipb_reason' );
 
                $params = $this->extractRequestParams();
                $this->requireMaxOneParameter( $params, 'userids', 'users' );
@@ -236,7 +237,7 @@ class ApiQueryUsers extends ApiQueryBase {
                                        $data[$key]['blockedby'] = $row->ipb_by_text;
                                        $data[$key]['blockedbyid'] = (int)$row->ipb_by;
                                        $data[$key]['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
-                                       $data[$key]['blockreason'] = $row->ipb_reason;
+                                       $data[$key]['blockreason'] = $commentStore->getComment( $row )->text;
                                        $data[$key]['blockexpiry'] = $row->ipb_expiry;
                                }
 
index 9883480..2ab8524 100644 (file)
@@ -34,6 +34,8 @@ use MediaWiki\MediaWikiServices;
  */
 class ApiQueryWatchlist extends ApiQueryGeneratorBase {
 
+       private $commentStore;
+
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'wl' );
        }
@@ -85,6 +87,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                        $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
                                }
                        }
+
+                       if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                               $this->commentStore = new CommentStore( 'rc_comment' );
+                       }
                }
 
                $options = [
@@ -353,12 +359,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                Revision::DELETED_COMMENT,
                                $user
                        ) ) {
-                               if ( $this->fld_comment && isset( $recentChangeInfo['rc_comment'] ) ) {
-                                       $vals['comment'] = $recentChangeInfo['rc_comment'];
+                               $comment = $this->commentStore->getComment( $recentChangeInfo )->text;
+                               if ( $this->fld_comment ) {
+                                       $vals['comment'] = $comment;
                                }
 
-                               if ( $this->fld_parsedcomment && isset( $recentChangeInfo['rc_comment'] ) ) {
-                                       $vals['parsedcomment'] = Linker::formatComment( $recentChangeInfo['rc_comment'], $title );
+                               if ( $this->fld_parsedcomment ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
                                }
                        }
                }
index 6272f4e..17881ef 100644 (file)
        "apihelp-query+revisions+base-param-section": "Recuperar solamente el contenido de este número de sección.",
        "apihelp-query+revisions+base-param-contentformat": "Formato de serialización utilizado para <var>$1difftotext</var> y esperado para la salida de contenido.",
        "apihelp-query+search-summary": "Realizar una búsqueda de texto completa.",
-       "apihelp-query+search-param-namespace": "Buscar sólo en estos espacios de nombres.",
+       "apihelp-query+search-param-namespace": "Buscar solo en estos espacios de nombres.",
        "apihelp-query+search-param-what": "Tipo de búsqueda que realizar.",
        "apihelp-query+search-param-info": "Qué metadatos devolver.",
        "apihelp-query+search-param-prop": "Qué propiedades se devolverán:",
index efee015..9a16e81 100644 (file)
@@ -29,6 +29,8 @@
        "apihelp-compare-param-toid": "Aldaratzeko bigarren orri IDa.",
        "apihelp-compare-param-torev": "Aldaratzeko bigarren berrikusketa.",
        "apihelp-compare-param-prop": "Hartu beharreko informazio zatiak.",
+       "apihelp-compare-paramvalue-prop-diff": "HTML diff-a",
+       "apihelp-compare-paramvalue-prop-diffsize": "HTML diff-aren tamainia, byte-tan",
        "apihelp-compare-example-1": "1. eta 2. berrikusketen arteko \"diff\"-a sortu.",
        "apihelp-createaccount-summary": "Erabiltzaile kontu berria sortu.",
        "apihelp-createaccount-param-name": "Erabiltzaile izena.",
        "apihelp-feedrecentchanges-example-simple": "Erakutsi aldaketa berriak",
        "apihelp-feedrecentchanges-example-30days": "Erakutsi aldaketa berriak 30 egunez",
        "apihelp-feedwatchlist-param-feedformat": "Produktuaren formatua.",
+       "apihelp-filerevert-summary": "Artxibo bat bertsio zaharrera bueltatu.",
        "apihelp-filerevert-param-comment": "Iruzkina igo.",
        "apihelp-help-example-recursive": "Laguntza guztia orrialde batean.",
        "apihelp-imagerotate-summary": "Irudi bat edo gehiago biratu.",
        "apihelp-imagerotate-param-rotation": "Irudia erloju-orratzen norabidean biratzeko graduak.",
        "apihelp-import-param-summary": "Inportazioaren laburpena.",
        "apihelp-import-param-xml": "XML fitxategia igo da.",
+       "apihelp-import-param-tags": "Aldatu etiketak sarrera aplikatzeko inportatzeko sarreran eta inportatutako orrialdeetan berrikuspena kentzeko.",
        "apihelp-login-param-name": "Erabiltzaile izena.",
        "apihelp-login-param-password": "Pasahitza.",
        "apihelp-login-param-domain": "Domeinua (hautazkoa).",
index 961cb48..913bd38 100644 (file)
@@ -73,13 +73,6 @@ class ChangesListBooleanFilter extends ChangesListFilter {
         */
        protected $activeValue;
 
-       /**
-        * Whether this filter is visible somewhere (legacy form or structured UI).
-        *
-        * @var bool $isVisible
-        */
-       protected $isVisible;
-
        /**
         * Create a new filter with the specified configuration.
         *
@@ -106,8 +99,6 @@ class ChangesListBooleanFilter extends ChangesListFilter {
         * * $filterDefinition['default'] bool Default
         * * $filterDefinition['activeValue'] bool This filter is considered active when
         *     its value is equal to its activeValue. Default is true.
-        * * $filterDefinition['isVisible'] bool This filter is visible in the legacy form or
-        *     structured UI. Default is true.
         * * $filterDefinition['priority'] int Priority integer.  Higher value means higher
         *     up in the group's filter list.
         * * $filterDefinition['queryCallable'] callable Callable accepting parameters, used
@@ -150,12 +141,6 @@ class ChangesListBooleanFilter extends ChangesListFilter {
                } else {
                        $this->activeValue = true;
                }
-
-               if ( isset( $filterDefinition['isVisible'] ) ) {
-                       $this->isVisible = $filterDefinition['isVisible'];
-               } else {
-                       $this->isVisible = true;
-               }
        }
 
        /**
@@ -268,11 +253,4 @@ class ChangesListBooleanFilter extends ChangesListFilter {
 
                return $opts[ $this->getName() ] === $this->activeValue;
        }
-
-       /**
-        * @return bool Whether this filter is visible anywhere
-        */
-       public function isVisible() {
-               return $this->isVisible;
-       }
 }
index f123363..588f602 100644 (file)
  * temporary:       not stored in the database
  *      notificationtimestamp
  *      numberofWatchingusers
+ *
+ * @todo Deprecate access to mAttribs (direct or via getAttributes). Right now
+ *  we're having to include both rc_comment and rc_comment_text/rc_comment_data
+ *  so random crap works right.
  */
 class RecentChange {
        // Constants for the rc_source field.  Extensions may also have
@@ -199,6 +203,8 @@ class RecentChange {
        /**
         * Return the list of recentchanges fields that should be selected to create
         * a new recentchanges object.
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        public static function selectFields() {
@@ -209,7 +215,6 @@ class RecentChange {
                        'rc_user_text',
                        'rc_namespace',
                        'rc_title',
-                       'rc_comment',
                        'rc_minor',
                        'rc_bot',
                        'rc_new',
@@ -227,7 +232,7 @@ class RecentChange {
                        'rc_log_type',
                        'rc_log_action',
                        'rc_params',
-               ];
+               ] + CommentStore::newKey( 'rc_comment' )->getFields();
        }
 
        # Accessors
@@ -322,8 +327,14 @@ class RecentChange {
                        unset( $this->mAttribs['rc_cur_id'] );
                }
 
+               # Convert mAttribs['rc_comment'] for CommentStore
+               $row = $this->mAttribs;
+               $comment = $row['rc_comment'];
+               unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
+               $row += CommentStore::newKey( 'rc_comment' )->insert( $dbw, $comment );
+
                # Insert new row
-               $dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
+               $dbw->insert( 'recentchanges', $row, __METHOD__ );
 
                # Set the ID
                $this->mAttribs['rc_id'] = $dbw->insertId();
@@ -586,7 +597,9 @@ class RecentChange {
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
-                       'rc_comment' => $comment,
+                       'rc_comment' => &$comment,
+                       'rc_comment_text' => &$comment,
+                       'rc_comment_data' => null,
                        'rc_this_oldid' => $newId,
                        'rc_last_oldid' => $oldId,
                        'rc_bot' => $bot ? 1 : 0,
@@ -659,7 +672,9 @@ class RecentChange {
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
-                       'rc_comment' => $comment,
+                       'rc_comment' => &$comment,
+                       'rc_comment_text' => &$comment,
+                       'rc_comment_data' => null,
                        'rc_this_oldid' => $newId,
                        'rc_last_oldid' => 0,
                        'rc_bot' => $bot ? 1 : 0,
@@ -789,7 +804,9 @@ class RecentChange {
                        'rc_cur_id' => $target->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
-                       'rc_comment' => $logComment,
+                       'rc_comment' => &$logComment,
+                       'rc_comment_text' => &$logComment,
+                       'rc_comment_data' => null,
                        'rc_this_oldid' => $revId,
                        'rc_last_oldid' => 0,
                        'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
@@ -862,7 +879,9 @@ class RecentChange {
                        'rc_cur_id' => $pageTitle->getArticleID(),
                        'rc_user' => $user ? $user->getId() : 0,
                        'rc_user_text' => $user ? $user->getName() : '',
-                       'rc_comment' => $comment,
+                       'rc_comment' => &$comment,
+                       'rc_comment_text' => &$comment,
+                       'rc_comment_data' => null,
                        'rc_this_oldid' => $newRevId,
                        'rc_last_oldid' => $oldRevId,
                        'rc_bot' => $bot ? 1 : 0,
@@ -922,6 +941,13 @@ class RecentChange {
                                $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
                        }
                }
+
+               $comment = CommentStore::newKey( 'rc_comment' )
+                       // Legacy because $row probably came from self::selectFields()
+                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
+               $this->mAttribs['rc_comment'] = &$comment;
+               $this->mAttribs['rc_comment_text'] = &$comment;
+               $this->mAttribs['rc_comment_data'] = null;
        }
 
        /**
@@ -931,6 +957,9 @@ class RecentChange {
         * @return mixed
         */
        public function getAttribute( $name ) {
+               if ( $name === 'rc_comment' ) {
+                       return CommentStore::newKey( 'rc_comment' )->getComment( $this->mAttribs, true )->text;
+               }
                return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
        }
 
index e0ebaa2..6e2a5a4 100644 (file)
@@ -260,7 +260,7 @@ class WikiExporter {
        protected function dumpFrom( $cond = '', $orderRevs = false ) {
                # For logging dumps...
                if ( $this->history & self::LOGS ) {
-                       $where = [ 'user_id = log_user' ];
+                       $where = [];
                        # Hide private logs
                        $hideLogs = LogEventsList::getExcludeClause( $this->db );
                        if ( $hideLogs ) {
@@ -277,12 +277,16 @@ class WikiExporter {
                                $prev = $this->db->bufferResults( false );
                        }
                        $result = null; // Assuring $result is not undefined, if exception occurs early
+
+                       $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+
                        try {
-                               $result = $this->db->select( [ 'logging', 'user' ],
-                                       [ "{$logging}.*", 'user_name' ], // grab the user name
+                               $result = $this->db->select( [ 'logging', 'user' ] + $commentQuery['tables'],
+                                       [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'], // grab the user name
                                        $where,
                                        __METHOD__,
-                                       [ 'ORDER BY' => 'log_id', 'USE INDEX' => [ 'logging' => 'PRIMARY' ] ]
+                                       [ 'ORDER BY' => 'log_id', 'USE INDEX' => [ 'logging' => 'PRIMARY' ] ],
+                                       [ 'user' => [ 'JOIN', 'user_id = log_user' ] ] + $commentQuery['joins']
                                );
                                $this->outputLogStream( $result );
                                if ( $this->buffer == self::STREAM ) {
@@ -395,8 +399,17 @@ class WikiExporter {
                                Hooks::run( 'ModifyExportQuery',
                                                [ $this->db, &$tables, &$cond, &$opts, &$join ] );
 
+                               $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
+
                                # Do the query!
-                               $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join );
+                               $result = $this->db->select(
+                                       $tables + $commentQuery['tables'],
+                                       [ '*' ] + $commentQuery['fields'],
+                                       $cond,
+                                       __METHOD__,
+                                       $opts,
+                                       $join + $commentQuery['joins']
+                               );
                                # Output dump results
                                $this->outputPageStream( $result );
 
index 943408c..990f16d 100644 (file)
@@ -218,8 +218,11 @@ class XmlDumpWriter {
                }
                if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_COMMENT ) ) {
                        $out .= "      " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
-               } elseif ( $row->rev_comment != '' ) {
-                       $out .= "      " . Xml::elementClean( 'comment', [], strval( $row->rev_comment ) ) . "\n";
+               } else {
+                       $comment = CommentStore::newKey( 'rev_comment' )->getComment( $row )->text;
+                       if ( $comment != '' ) {
+                               $out .= "      " . Xml::elementClean( 'comment', [], strval( $comment ) ) . "\n";
+                       }
                }
 
                if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) {
@@ -299,8 +302,11 @@ class XmlDumpWriter {
 
                if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
                        $out .= "    " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
-               } elseif ( $row->log_comment != '' ) {
-                       $out .= "    " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
+               } else {
+                       $comment = CommentStore::newKey( 'log_comment' )->getComment( $row )->text;
+                       if ( $comment != '' ) {
+                               $out .= "    " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
+                       }
                }
 
                $out .= "    " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
index 6984d48..758fb4b 100644 (file)
@@ -215,6 +215,8 @@ class ArchivedFile {
 
        /**
         * Fields in the filearchive table
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        static function selectFields() {
@@ -232,14 +234,13 @@ class ArchivedFile {
                        'fa_media_type',
                        'fa_major_mime',
                        'fa_minor_mime',
-                       'fa_description',
                        'fa_user',
                        'fa_user_text',
                        'fa_timestamp',
                        'fa_deleted',
                        'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
                        'fa_sha1',
-               ];
+               ] + CommentStore::newKey( 'fa_description' )->getFields();
        }
 
        /**
@@ -261,7 +262,9 @@ class ArchivedFile {
                $this->metadata = $row->fa_metadata;
                $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
                $this->media_type = $row->fa_media_type;
-               $this->description = $row->fa_description;
+               $this->description = CommentStore::newKey( 'fa_description' )
+                       // Legacy because $row probably came from self::selectFields()
+                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text;
                $this->user = $row->fa_user;
                $this->user_text = $row->fa_user_text;
                $this->timestamp = $row->fa_timestamp;
index 33177d3..904c932 100644 (file)
@@ -193,6 +193,8 @@ class LocalFile extends File {
 
        /**
         * Fields in the image table
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        static function selectFields() {
@@ -206,12 +208,11 @@ class LocalFile extends File {
                        'img_media_type',
                        'img_major_mime',
                        'img_minor_mime',
-                       'img_description',
                        'img_user',
                        'img_user_text',
                        'img_timestamp',
                        'img_sha1',
-               ];
+               ] + CommentStore::newKey( 'img_description' )->getFields();
        }
 
        /**
@@ -1299,6 +1300,8 @@ class LocalFile extends File {
        function recordUpload2(
                $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
        ) {
+               global $wgCommentTableSchemaMigrationStage;
+
                if ( is_null( $user ) ) {
                        global $wgUser;
                        $user = $wgUser;
@@ -1334,6 +1337,9 @@ class LocalFile extends File {
                # Test to see if the row exists using INSERT IGNORE
                # This avoids race conditions by locking the row until the commit, and also
                # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
+               $commentStore = new CommentStore( 'img_description' );
+               list( $commentFields, $commentCallback ) =
+                       $commentStore->insertWithTempTable( $dbw, $comment );
                $dbw->insert( 'image',
                        [
                                'img_name' => $this->getName(),
@@ -1345,17 +1351,16 @@ class LocalFile extends File {
                                'img_major_mime' => $this->major_mime,
                                'img_minor_mime' => $this->minor_mime,
                                'img_timestamp' => $timestamp,
-                               'img_description' => $comment,
                                'img_user' => $user->getId(),
                                'img_user_text' => $user->getName(),
                                'img_metadata' => $dbw->encodeBlob( $this->metadata ),
                                'img_sha1' => $this->sha1
-                       ],
+                       ] + $commentFields,
                        __METHOD__,
                        'IGNORE'
                );
-
                $reupload = ( $dbw->affectedRows() == 0 );
+
                if ( $reupload ) {
                        if ( $allowTimeKludge ) {
                                # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
@@ -1376,33 +1381,65 @@ class LocalFile extends File {
                                }
                        }
 
+                       $tables = [ 'image' ];
+                       $fields = [
+                               'oi_name' => 'img_name',
+                               'oi_archive_name' => $dbw->addQuotes( $oldver ),
+                               'oi_size' => 'img_size',
+                               'oi_width' => 'img_width',
+                               'oi_height' => 'img_height',
+                               'oi_bits' => 'img_bits',
+                               'oi_timestamp' => 'img_timestamp',
+                               'oi_user' => 'img_user',
+                               'oi_user_text' => 'img_user_text',
+                               'oi_metadata' => 'img_metadata',
+                               'oi_media_type' => 'img_media_type',
+                               'oi_major_mime' => 'img_major_mime',
+                               'oi_minor_mime' => 'img_minor_mime',
+                               'oi_sha1' => 'img_sha1',
+                       ];
+                       $joins = [];
+
+                       if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               $fields['oi_description'] = 'img_description';
+                       }
+                       if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               $tables[] = 'image_comment_temp';
+                               $fields['oi_description_id'] = 'imgcomment_description_id';
+                               $joins['image_comment_temp'] = [
+                                       $wgCommentTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                       [ 'imgcomment_name = img_name' ]
+                               ];
+                       }
+
+                       if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
+                               $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
+                       ) {
+                               // Upgrade any rows that are still old-style. Otherwise an upgrade
+                               // might be missed if a deletion happens while the migration script
+                               // is running.
+                               $res = $dbw->select(
+                                       [ 'image', 'image_comment_temp' ],
+                                       [ 'img_name', 'img_description' ],
+                                       [ 'img_name' => $this->getName(), 'imgcomment_name' => null ],
+                                       __METHOD__,
+                                       [],
+                                       [ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
+                               );
+                               foreach ( $res as $row ) {
+                                       list( , $callback ) = $commentStore->insertWithTempTable( $dbw, $row->img_description );
+                                       $callback( $row->img_name );
+                               }
+                       }
+
                        # (T36993) Note: $oldver can be empty here, if the previous
                        # version of the file was broken. Allow registration of the new
                        # version to continue anyway, because that's better than having
                        # an image that's not fixable by user operations.
                        # Collision, this is an update of a file
                        # Insert previous contents into oldimage
-                       $dbw->insertSelect( 'oldimage', 'image',
-                               [
-                                       'oi_name' => 'img_name',
-                                       'oi_archive_name' => $dbw->addQuotes( $oldver ),
-                                       'oi_size' => 'img_size',
-                                       'oi_width' => 'img_width',
-                                       'oi_height' => 'img_height',
-                                       'oi_bits' => 'img_bits',
-                                       'oi_timestamp' => 'img_timestamp',
-                                       'oi_description' => 'img_description',
-                                       'oi_user' => 'img_user',
-                                       'oi_user_text' => 'img_user_text',
-                                       'oi_metadata' => 'img_metadata',
-                                       'oi_media_type' => 'img_media_type',
-                                       'oi_major_mime' => 'img_major_mime',
-                                       'oi_minor_mime' => 'img_minor_mime',
-                                       'oi_sha1' => 'img_sha1'
-                               ],
-                               [ 'img_name' => $this->getName() ],
-                               __METHOD__
-                       );
+                       $dbw->insertSelect( 'oldimage', $tables, $fields,
+                               [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
 
                        # Update the current image row
                        $dbw->update( 'image',
@@ -1415,16 +1452,20 @@ class LocalFile extends File {
                                        'img_major_mime' => $this->major_mime,
                                        'img_minor_mime' => $this->minor_mime,
                                        'img_timestamp' => $timestamp,
-                                       'img_description' => $comment,
                                        'img_user' => $user->getId(),
                                        'img_user_text' => $user->getName(),
                                        'img_metadata' => $dbw->encodeBlob( $this->metadata ),
                                        'img_sha1' => $this->sha1
-                               ],
+                               ] + $commentFields,
                                [ 'img_name' => $this->getName() ],
                                __METHOD__
                        );
+                       if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               // So $commentCallback can insert the new row
+                               $dbw->delete( 'image_comment_temp', [ 'imgcomment_name' => $this->getName() ], __METHOD__ );
+                       }
                }
+               $commentCallback( $this->getName() );
 
                $descTitle = $this->getTitle();
                $descId = $descTitle->getArticleID();
@@ -2255,8 +2296,16 @@ class LocalFileDeleteBatch {
        }
 
        protected function doDBInserts() {
+               global $wgCommentTableSchemaMigrationStage;
+
                $now = time();
                $dbw = $this->file->repo->getMasterDB();
+
+               $commentStoreImgDesc = new CommentStore( 'img_description' );
+               $commentStoreOiDesc = new CommentStore( 'oi_description' );
+               $commentStoreFaDesc = new CommentStore( 'fa_description' );
+               $commentStoreFaReason = new CommentStore( 'fa_deleted_reason' );
+
                $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
                $encUserId = $dbw->addQuotes( $this->user->getId() );
                $encReason = $dbw->addQuotes( $this->reason );
@@ -2274,39 +2323,70 @@ class LocalFileDeleteBatch {
                }
 
                if ( $deleteCurrent ) {
-                       $dbw->insertSelect(
-                               'filearchive',
-                               'image',
-                               [
-                                       'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => $dbw->conditional(
-                                               [ 'img_sha1' => '' ],
-                                               $dbw->addQuotes( '' ),
-                                               $dbw->buildConcat( [ "img_sha1", $encExt ] )
-                                       ),
-                                       'fa_deleted_user' => $encUserId,
-                                       'fa_deleted_timestamp' => $encTimestamp,
-                                       'fa_deleted_reason' => $encReason,
-                                       'fa_deleted' => $this->suppress ? $bitfield : 0,
-                                       'fa_name' => 'img_name',
-                                       'fa_archive_name' => 'NULL',
-                                       'fa_size' => 'img_size',
-                                       'fa_width' => 'img_width',
-                                       'fa_height' => 'img_height',
-                                       'fa_metadata' => 'img_metadata',
-                                       'fa_bits' => 'img_bits',
-                                       'fa_media_type' => 'img_media_type',
-                                       'fa_major_mime' => 'img_major_mime',
-                                       'fa_minor_mime' => 'img_minor_mime',
-                                       'fa_description' => 'img_description',
-                                       'fa_user' => 'img_user',
-                                       'fa_user_text' => 'img_user_text',
-                                       'fa_timestamp' => 'img_timestamp',
-                                       'fa_sha1' => 'img_sha1'
-                               ],
-                               [ 'img_name' => $this->file->getName() ],
-                               __METHOD__
-                       );
+                       $tables = [ 'image' ];
+                       $fields = [
+                               'fa_storage_group' => $encGroup,
+                               'fa_storage_key' => $dbw->conditional(
+                                       [ 'img_sha1' => '' ],
+                                       $dbw->addQuotes( '' ),
+                                       $dbw->buildConcat( [ "img_sha1", $encExt ] )
+                               ),
+                               'fa_deleted_user' => $encUserId,
+                               'fa_deleted_timestamp' => $encTimestamp,
+                               'fa_deleted' => $this->suppress ? $bitfield : 0,
+                               'fa_name' => 'img_name',
+                               'fa_archive_name' => 'NULL',
+                               'fa_size' => 'img_size',
+                               'fa_width' => 'img_width',
+                               'fa_height' => 'img_height',
+                               'fa_metadata' => 'img_metadata',
+                               'fa_bits' => 'img_bits',
+                               'fa_media_type' => 'img_media_type',
+                               'fa_major_mime' => 'img_major_mime',
+                               'fa_minor_mime' => 'img_minor_mime',
+                               'fa_user' => 'img_user',
+                               'fa_user_text' => 'img_user_text',
+                               'fa_timestamp' => 'img_timestamp',
+                               'fa_sha1' => 'img_sha1'
+                       ];
+                       $joins = [];
+
+                       $fields += $commentStoreFaReason->insert( $dbw, $encReason );
+
+                       if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               $fields['fa_description'] = 'img_description';
+                       }
+                       if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               $tables[] = 'image_comment_temp';
+                               $fields['fa_description_id'] = 'imgcomment_description_id';
+                               $joins['image_comment_temp'] = [
+                                       $wgCommentTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                       [ 'imgcomment_name = img_name' ]
+                               ];
+                       }
+
+                       if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
+                               $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
+                       ) {
+                               // Upgrade any rows that are still old-style. Otherwise an upgrade
+                               // might be missed if a deletion happens while the migration script
+                               // is running.
+                               $res = $dbw->select(
+                                       [ 'image', 'image_comment_temp' ],
+                                       [ 'img_name', 'img_description' ],
+                                       [ 'img_name' => $this->file->getName(), 'imgcomment_name' => null ],
+                                       __METHOD__,
+                                       [],
+                                       [ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
+                               );
+                               foreach ( $res as $row ) {
+                                       list( , $callback ) = $commentStoreImgDesc->insertWithTempTable( $dbw, $row->img_description );
+                                       $callback( $row->img_name );
+                               }
+                       }
+
+                       $dbw->insertSelect( 'filearchive', $tables, $fields,
+                               [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
                }
 
                if ( count( $oldRels ) ) {
@@ -2321,34 +2401,38 @@ class LocalFileDeleteBatch {
                                [ 'FOR UPDATE' ]
                        );
                        $rowsInsert = [];
-                       foreach ( $res as $row ) {
-                               $rowsInsert[] = [
-                                       // Deletion-specific fields
-                                       'fa_storage_group' => 'deleted',
-                                       'fa_storage_key' => ( $row->oi_sha1 === '' )
+                       if ( $res->numRows() ) {
+                               $reason = $commentStoreFaReason->createComment( $dbw, $this->reason );
+                               foreach ( $res as $row ) {
+                                       // Legacy from OldLocalFile::selectFields() just above
+                                       $comment = $commentStoreOiDesc->getCommentLegacy( $dbw, $row );
+                                       $rowsInsert[] = [
+                                               // Deletion-specific fields
+                                               'fa_storage_group' => 'deleted',
+                                               'fa_storage_key' => ( $row->oi_sha1 === '' )
                                                ? ''
                                                : "{$row->oi_sha1}{$dotExt}",
-                                       'fa_deleted_user' => $this->user->getId(),
-                                       'fa_deleted_timestamp' => $dbw->timestamp( $now ),
-                                       'fa_deleted_reason' => $this->reason,
-                                       // Counterpart fields
-                                       'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
-                                       'fa_name' => $row->oi_name,
-                                       'fa_archive_name' => $row->oi_archive_name,
-                                       'fa_size' => $row->oi_size,
-                                       'fa_width' => $row->oi_width,
-                                       'fa_height' => $row->oi_height,
-                                       'fa_metadata' => $row->oi_metadata,
-                                       'fa_bits' => $row->oi_bits,
-                                       'fa_media_type' => $row->oi_media_type,
-                                       'fa_major_mime' => $row->oi_major_mime,
-                                       'fa_minor_mime' => $row->oi_minor_mime,
-                                       'fa_description' => $row->oi_description,
-                                       'fa_user' => $row->oi_user,
-                                       'fa_user_text' => $row->oi_user_text,
-                                       'fa_timestamp' => $row->oi_timestamp,
-                                       'fa_sha1' => $row->oi_sha1
-                               ];
+                                               'fa_deleted_user' => $this->user->getId(),
+                                               'fa_deleted_timestamp' => $dbw->timestamp( $now ),
+                                               // Counterpart fields
+                                               'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
+                                               'fa_name' => $row->oi_name,
+                                               'fa_archive_name' => $row->oi_archive_name,
+                                               'fa_size' => $row->oi_size,
+                                               'fa_width' => $row->oi_width,
+                                               'fa_height' => $row->oi_height,
+                                               'fa_metadata' => $row->oi_metadata,
+                                               'fa_bits' => $row->oi_bits,
+                                               'fa_media_type' => $row->oi_media_type,
+                                               'fa_major_mime' => $row->oi_major_mime,
+                                               'fa_minor_mime' => $row->oi_minor_mime,
+                                               'fa_user' => $row->oi_user,
+                                               'fa_user_text' => $row->oi_user_text,
+                                               'fa_timestamp' => $row->oi_timestamp,
+                                               'fa_sha1' => $row->oi_sha1
+                                       ] + $commentStoreFaReason->insert( $dbw, $reason )
+                                       + $commentStoreFaDesc->insert( $dbw, $comment );
+                               }
                        }
 
                        $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
@@ -2356,6 +2440,8 @@ class LocalFileDeleteBatch {
        }
 
        function doDBDeletes() {
+               global $wgUpdateCompatibleMetadata;
+
                $dbw = $this->file->repo->getMasterDB();
                list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 
@@ -2369,6 +2455,11 @@ class LocalFileDeleteBatch {
 
                if ( $deleteCurrent ) {
                        $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
+                       if ( $wgUpdateCompatibleMetadata > MIGRATION_OLD ) {
+                               $dbw->delete(
+                                       'image_comment_temp', [ 'imgcomment_name' => $this->file->getName() ], __METHOD__
+                               );
+                       }
                }
        }
 
@@ -2537,6 +2628,11 @@ class LocalFileRestoreBatch {
                $lockOwnsTrx = $this->file->lock();
 
                $dbw = $this->file->repo->getMasterDB();
+
+               $commentStoreImgDesc = new CommentStore( 'img_description' );
+               $commentStoreOiDesc = new CommentStore( 'oi_description' );
+               $commentStoreFaDesc = new CommentStore( 'fa_description' );
+
                $status = $this->file->repo->newGood();
 
                $exists = (bool)$dbw->selectField( 'image', '1',
@@ -2621,9 +2717,13 @@ class LocalFileRestoreBatch {
                                ];
                        }
 
+                       // Legacy from ArchivedFile::selectFields() just above
+                       $comment = $commentStoreFaDesc->getCommentLegacy( $dbw, $row );
                        if ( $first && !$exists ) {
                                // This revision will be published as the new current version
                                $destRel = $this->file->getRel();
+                               list( $commentFields, $commentCallback ) =
+                                       $commentStoreImgDesc->insertWithTempTable( $dbw, $comment );
                                $insertCurrent = [
                                        'img_name' => $row->fa_name,
                                        'img_size' => $row->fa_size,
@@ -2634,12 +2734,11 @@ class LocalFileRestoreBatch {
                                        'img_media_type' => $props['media_type'],
                                        'img_major_mime' => $props['major_mime'],
                                        'img_minor_mime' => $props['minor_mime'],
-                                       'img_description' => $row->fa_description,
                                        'img_user' => $row->fa_user,
                                        'img_user_text' => $row->fa_user_text,
                                        'img_timestamp' => $row->fa_timestamp,
                                        'img_sha1' => $sha1
-                               ];
+                               ] + $commentFields;
 
                                // The live (current) version cannot be hidden!
                                if ( !$this->unsuppress && $row->fa_deleted ) {
@@ -2671,7 +2770,6 @@ class LocalFileRestoreBatch {
                                        'oi_width' => $row->fa_width,
                                        'oi_height' => $row->fa_height,
                                        'oi_bits' => $row->fa_bits,
-                                       'oi_description' => $row->fa_description,
                                        'oi_user' => $row->fa_user,
                                        'oi_user_text' => $row->fa_user_text,
                                        'oi_timestamp' => $row->fa_timestamp,
@@ -2680,7 +2778,8 @@ class LocalFileRestoreBatch {
                                        'oi_major_mime' => $props['major_mime'],
                                        'oi_minor_mime' => $props['minor_mime'],
                                        'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
-                                       'oi_sha1' => $sha1 ];
+                                       'oi_sha1' => $sha1
+                               ] + $commentStoreOiDesc->insert( $dbw, $comment );
                        }
 
                        $deleteIds[] = $row->fa_id;
@@ -2738,6 +2837,7 @@ class LocalFileRestoreBatch {
                // This is not ideal, which is why it's important to lock the image row.
                if ( $insertCurrent ) {
                        $dbw->insert( 'image', $insertCurrent, __METHOD__ );
+                       $commentCallback( $insertCurrent['img_name'] );
                }
 
                if ( $insertBatch ) {
index dfaae73..b46e1e4 100644 (file)
@@ -103,6 +103,8 @@ class OldLocalFile extends LocalFile {
 
        /**
         * Fields in the oldimage table
+        * @todo Deprecate this in favor of a method that returns tables and joins
+        *  as well, and use CommentStore::getJoin().
         * @return array
         */
        static function selectFields() {
@@ -117,13 +119,12 @@ class OldLocalFile extends LocalFile {
                        'oi_media_type',
                        'oi_major_mime',
                        'oi_minor_mime',
-                       'oi_description',
                        'oi_user',
                        'oi_user_text',
                        'oi_timestamp',
                        'oi_deleted',
                        'oi_sha1',
-               ];
+               ] + CommentStore::newKey( 'oi_description' )->getFields();
        }
 
        /**
@@ -367,6 +368,7 @@ class OldLocalFile extends LocalFile {
                        return false;
                }
 
+               $commentFields = CommentStore::newKey( 'oi_description' )->insert( $dbw, $comment );
                $dbw->insert( 'oldimage',
                        [
                                'oi_name' => $this->getName(),
@@ -376,7 +378,6 @@ class OldLocalFile extends LocalFile {
                                'oi_height' => intval( $props['height'] ),
                                'oi_bits' => $props['bits'],
                                'oi_timestamp' => $dbw->timestamp( $timestamp ),
-                               'oi_description' => $comment,
                                'oi_user' => $user->getId(),
                                'oi_user_text' => $user->getName(),
                                'oi_metadata' => $props['metadata'],
@@ -384,7 +385,7 @@ class OldLocalFile extends LocalFile {
                                'oi_major_mime' => $props['major_mime'],
                                'oi_minor_mime' => $props['minor_mime'],
                                'oi_sha1' => $props['sha1'],
-                       ], __METHOD__
+                       ] + $commentFields, __METHOD__
                );
 
                return true;
index 2099709..9066079 100644 (file)
@@ -813,7 +813,7 @@ class WikiImporter {
                $this->debug( "Enter revision handler" );
                $revisionInfo = [];
 
-               $normalFields = [ 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text' ];
+               $normalFields = [ 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text', 'sha1' ];
 
                $skip = false;
 
@@ -916,6 +916,9 @@ class WikiImporter {
                } else {
                        $revision->setUsername( 'Unknown user' );
                }
+               if ( isset( $revisionInfo['sha1'] ) ) {
+                       $revision->setSha1Base36( $revisionInfo['sha1'] );
+               }
                $revision->setNoUpdates( $this->mNoUpdates );
 
                return $this->revisionCallback( $revision );
index f6becb9..93a92ef 100644 (file)
@@ -607,11 +607,12 @@ class WikiRevision {
                        $pageId = $page->getId();
                        $created = false;
 
+                       // Note: sha1 has been in XML dumps since 2012. If you have an
+                       // older dump, the duplicate detection here won't work.
                        $prior = $dbw->selectField( 'revision', '1',
                                [ 'rev_page' => $pageId,
                                        'rev_timestamp' => $dbw->timestamp( $this->timestamp ),
-                                       'rev_user_text' => $userText,
-                                       'rev_comment' => $this->getComment() ],
+                                       'rev_sha1' => $this->sha1base36 ],
                                __METHOD__
                        );
                        if ( $prior ) {
@@ -708,7 +709,6 @@ class WikiRevision {
                                'log_timestamp' => $dbw->timestamp( $this->timestamp ),
                                'log_namespace' => $this->getTitle()->getNamespace(),
                                'log_title' => $this->getTitle()->getDBkey(),
-                               'log_comment' => $this->getComment(),
                                # 'log_user_text' => $this->user_text,
                                'log_params' => $this->params ],
                        __METHOD__
@@ -730,9 +730,8 @@ class WikiRevision {
                        'log_user_text' => $userText,
                        'log_namespace' => $this->getTitle()->getNamespace(),
                        'log_title' => $this->getTitle()->getDBkey(),
-                       'log_comment' => $this->getComment(),
                        'log_params' => $this->params
-               ];
+               ] + CommentStore::newKey( 'log_comment' )->insert( $dbw, $this->getComment() );
                $dbw->insert( 'logging', $data, __METHOD__ );
 
                return true;
index b832d45..645fa8a 100644 (file)
@@ -1190,4 +1190,25 @@ abstract class DatabaseUpdater {
                        $wgContentHandlerUseDB = $this->holdContentHandlerUseDB;
                }
        }
+
+       /**
+        * Migrate comments to the new 'comment' table
+        * @since 1.30
+        */
+       protected function migrateComments() {
+               global $wgCommentTableSchemaMigrationStage;
+               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
+                       !$this->updateRowExists( 'MigrateComments' )
+               ) {
+                       $this->output(
+                               "Migrating comments to the 'comments' table, printing progress markers. For large\n" .
+                               "databases, you may want to hit Ctrl-C and do this manually with\n" .
+                               "maintenance/migrateComments.php.\n"
+                       );
+                       $task = $this->maintenance->runChild( 'MigrateComments', 'migrateComments.php' );
+                       $task->execute();
+                       $this->output( "done.\n" );
+               }
+       }
+
 }
index 7b51ed7..c591974 100644 (file)
@@ -326,6 +326,8 @@ class MysqlUpdater extends DatabaseUpdater {
                                'patch-user_former_groups-fix-pk.sql' ],
                        [ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
                                'patch-user_properties-fix-pk.sql' ],
+                       [ 'addTable', 'comment', 'patch-comment-table.sql' ],
+                       [ 'migrateComments' ],
                ];
        }
 
index d8db6a2..e5a5c94 100644 (file)
@@ -455,6 +455,32 @@ class PostgresUpdater extends DatabaseUpdater {
 
                        // 1.30
                        [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
+                       [ 'setDefault', 'revision', 'rev_comment', '' ],
+                       [ 'changeNullableField', 'revision', 'rev_comment', 'NOT NULL', true ],
+                       [ 'setDefault', 'archive', 'ar_comment', '' ],
+                       [ 'changeNullableField', 'archive', 'ar_comment', 'NOT NULL', true ],
+                       [ 'addPgField', 'archive', 'ar_comment_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'ipblocks', 'ipb_reason', '' ],
+                       [ 'addPgField', 'ipblocks', 'ipb_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'image', 'img_description', '' ],
+                       [ 'setDefault', 'oldimage', 'oi_description', '' ],
+                       [ 'changeNullableField', 'oldimage', 'oi_description', 'NOT NULL', true ],
+                       [ 'addPgField', 'oldimage', 'oi_description_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'filearchive', 'fa_deleted_reason', '' ],
+                       [ 'changeNullableField', 'filearchive', 'fa_deleted_reason', 'NOT NULL', true ],
+                       [ 'addPgField', 'filearchive', 'fa_deleted_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'filearchive', 'fa_description', '' ],
+                       [ 'addPgField', 'filearchive', 'fa_description_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'recentchanges', 'rc_comment', '' ],
+                       [ 'changeNullableField', 'recentchanges', 'rc_comment', 'NOT NULL', true ],
+                       [ 'addPgField', 'recentchanges', 'rc_comment_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'logging', 'log_comment', '' ],
+                       [ 'changeNullableField', 'logging', 'log_comment', 'NOT NULL', true ],
+                       [ 'addPgField', 'logging', 'log_comment_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'protected_titles', 'pt_reason', '' ],
+                       [ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ],
+                       [ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'addTable', 'comment', 'patch-comment-table.sql' ],
                ];
        }
 
@@ -761,7 +787,7 @@ END;
                }
        }
 
-       protected function changeNullableField( $table, $field, $null ) {
+       protected function changeNullableField( $table, $field, $null, $update = false ) {
                $fi = $this->db->fieldInfo( $table, $field );
                if ( is_null( $fi ) ) {
                        $this->output( "...ERROR: expected column $table.$field to exist\n" );
@@ -771,6 +797,9 @@ END;
                        # # It's NULL - does it need to be NOT NULL?
                        if ( 'NOT NULL' === $null ) {
                                $this->output( "Changing '$table.$field' to not allow NULLs\n" );
+                               if ( $update ) {
+                                       $this->db->query( "UPDATE $table SET $field = DEFAULT WHERE $field IS NULL" );
+                               }
                                $this->db->query( "ALTER TABLE $table ALTER $field SET NOT NULL" );
                        } else {
                                $this->output( "...column '$table.$field' is already set as NULL\n" );
index 95014a4..e79dcb1 100644 (file)
@@ -190,6 +190,8 @@ class SqliteUpdater extends DatabaseUpdater {
                                'patch-user_former_groups-fix-pk.sql' ],
                        [ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
                                'patch-user_properties-fix-pk.sql' ],
+                       [ 'addTable', 'comment', 'patch-comment-table.sql' ],
+                       [ 'migrateComments' ],
                ];
        }
 
index 021aa5d..85b23fe 100644 (file)
        "config-pingback-help": "Si seleccionas esta opción, MediaWiki enviará periódicamente a https://www.mediawiki.org datos básicos sobre esta instancia de MediaWiki. Se trata de datos tales como el tipo de sistema, la versión de PHP y la base de datos elegida. La Fundación Wikimedia comparte estos datos con los desarrolladores de MediaWiki para ayudar a guiar el desarrollo futuro. Se enviarán los siguientes datos para tu sistema:\n<pre>$1</pre>",
        "config-almost-done": "¡Ya casi has terminado!\nAhora puedes saltarte el resto de los pasos e instalar el wiki ya.",
        "config-optional-continue": "Hazme más preguntas.",
-       "config-optional-skip": "Ya estoy aburrido, sólo instala el wiki.",
+       "config-optional-skip": "Ya me aburrí. Tan solo instala el wiki.",
        "config-profile": "Perfil de derechos de usuario:",
        "config-profile-wiki": "Wiki abierto",
        "config-profile-no-anon": "Creación de cuenta requerida",
-       "config-profile-fishbowl": "Sólo editores autorizados",
+       "config-profile-fishbowl": "Solo editores autorizados",
        "config-profile-private": "Wiki privado",
        "config-profile-help": "Los wikis funcionan mejor cuando dejas que los edite tanta gente como sea posible.\nEn MediaWiki, es fácil revisar los cambios recientes y revertir los daños realizados por usuarios malintencionados o novatos.\nSin embargo, muchos han encontrado que MediaWiki es útil para una amplia variedad de funciones, y a veces no es fácil convencer a todos de los beneficios de la forma wiki.\nPor lo tanto tienes la elección.\n\nEl modelo <strong>{{int:config-profile-wiki}}</strong> permite que cualquiera pueda editar, sin siquiera iniciar sesión.\nUn wiki con <strong>{{int:config-profile-no-anon}}</strong> ofrece rendición de cuentas adicional, pero puede disuadir a colaboradores casuales.\n\nEl modelo <strong>{{int:config-profile-fishbowl}}</strong> permite editar a los usuarios autorizados, pero el público puede ver las páginas, incluyendo el historial.\nUn <strong>{{int:config-profile-private}}</strong> sólo permite ver páginas a los usuarios autorizados, el mismo grupo al que le está permitido editar.\n\nConfiguraciones más complejas de permisos de usuario están disponibles después de la instalación. Consulta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights la entrada correspondiente del manual].",
        "config-license": "Derechos de autor y licencia:",
index fa94fe5..6587304 100644 (file)
@@ -170,19 +170,21 @@ class DatabaseLogEntry extends LogEntryBase {
         * @return array
         */
        public static function getSelectQueryData() {
-               $tables = [ 'logging', 'user' ];
+               $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+
+               $tables = [ 'logging', 'user' ] + $commentQuery['tables'];
                $fields = [
                        'log_id', 'log_type', 'log_action', 'log_timestamp',
                        'log_user', 'log_user_text',
                        'log_namespace', 'log_title', // unused log_page
-                       'log_comment', 'log_params', 'log_deleted',
+                       'log_params', 'log_deleted',
                        'user_id', 'user_name', 'user_editcount',
-               ];
+               ] + $commentQuery['fields'];
 
                $joins = [
                        // IPs don't have an entry in user table
                        'user' => [ 'LEFT JOIN', 'log_user=user_id' ],
-               ];
+               ] + $commentQuery['joins'];
 
                return [
                        'tables' => $tables,
@@ -322,7 +324,7 @@ class DatabaseLogEntry extends LogEntryBase {
        }
 
        public function getComment() {
-               return $this->row->log_comment;
+               return CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text;
        }
 
        public function getDeleted() {
@@ -380,7 +382,9 @@ class RCDatabaseLogEntry extends DatabaseLogEntry {
        }
 
        public function getComment() {
-               return $this->row->rc_comment;
+               return CommentStore::newKey( 'rc_comment' )
+                       // Legacy because the row probably used RecentChange::selectFields()
+                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $this->row )->text;
        }
 
        public function getDeleted() {
@@ -624,12 +628,12 @@ class ManualLogEntry extends LogEntryBase {
                        'log_namespace' => $this->getTarget()->getNamespace(),
                        'log_title' => $this->getTarget()->getDBkey(),
                        'log_page' => $this->getTarget()->getArticleID(),
-                       'log_comment' => $comment,
                        'log_params' => LogEntryBase::makeParamBlob( $params ),
                ];
                if ( isset( $this->deleted ) ) {
                        $data['log_deleted'] = $this->deleted;
                }
+               $data += CommentStore::newKey( 'log_comment' )->insert( $dbw, $comment );
 
                $dbw->insert( 'logging', $data, __METHOD__ );
                $this->id = $dbw->insertId();
index a085e3e..257f76d 100644 (file)
@@ -104,9 +104,9 @@ class LogPage {
                        'log_namespace' => $this->target->getNamespace(),
                        'log_title' => $this->target->getDBkey(),
                        'log_page' => $this->target->getArticleID(),
-                       'log_comment' => $this->comment,
                        'log_params' => $this->params
                ];
+               $data += CommentStore::newKey( 'log_comment' )->insert( $dbw, $this->comment );
                $dbw->insert( 'logging', $data, __METHOD__ );
                $newId = $dbw->insertId();
 
index 11e1a30..f6580e9 100644 (file)
@@ -171,20 +171,21 @@ class PageArchive {
 
        /**
         * List the revisions of the given page. Returns result wrapper with
-        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+        * various archive table fields.
         *
         * @return ResultWrapper
         */
        public function listRevisions() {
                $dbr = wfGetDB( DB_REPLICA );
+               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
 
-               $tables = [ 'archive' ];
+               $tables = [ 'archive' ] + $commentQuery['tables'];
 
                $fields = [
                        'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
-                       'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+                       'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
                        'ar_page_id'
-               ];
+               ] + $commentQuery['fields'];
 
                if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
                        $fields[] = 'ar_content_format';
@@ -196,7 +197,7 @@ class PageArchive {
 
                $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
 
-               $join_conds = [];
+               $join_conds = [] + $commentQuery['joins'];
 
                ChangeTags::modifyDisplayQuery(
                        $tables,
@@ -248,11 +249,13 @@ class PageArchive {
         */
        public function getRevision( $timestamp ) {
                $dbr = wfGetDB( DB_REPLICA );
+               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+
+               $tables = [ 'archive' ] + $commentQuery['tables'];
 
                $fields = [
                        'ar_rev_id',
                        'ar_text',
-                       'ar_comment',
                        'ar_user',
                        'ar_user_text',
                        'ar_timestamp',
@@ -262,19 +265,27 @@ class PageArchive {
                        'ar_deleted',
                        'ar_len',
                        'ar_sha1',
-               ];
+               ] + $commentQuery['fields'];
 
                if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
                        $fields[] = 'ar_content_format';
                        $fields[] = 'ar_content_model';
                }
 
-               $row = $dbr->selectRow( 'archive',
+               $join_conds = [] + $commentQuery['joins'];
+
+               $row = $dbr->selectRow(
+                       $tables,
                        $fields,
-                       [ 'ar_namespace' => $this->title->getNamespace(),
+                       [
+                               'ar_namespace' => $this->title->getNamespace(),
                                'ar_title' => $this->title->getDBkey(),
-                               'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
-                       __METHOD__ );
+                               'ar_timestamp' => $dbr->timestamp( $timestamp )
+                       ],
+                       __METHOD__,
+                       [],
+                       $join_conds
+               );
 
                if ( $row ) {
                        return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
@@ -552,12 +563,15 @@ class PageArchive {
                        $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
                }
 
+               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+
+               $tables = [ 'archive', 'revision' ] + $commentQuery['tables'];
+
                $fields = [
                        'ar_id',
                        'ar_rev_id',
                        'rev_id',
                        'ar_text',
-                       'ar_comment',
                        'ar_user',
                        'ar_user_text',
                        'ar_timestamp',
@@ -568,24 +582,28 @@ class PageArchive {
                        'ar_page_id',
                        'ar_len',
                        'ar_sha1'
-               ];
+               ] + $commentQuery['fields'];
 
                if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
                        $fields[] = 'ar_content_format';
                        $fields[] = 'ar_content_model';
                }
 
+               $join_conds = [
+                       'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ],
+               ] + $commentQuery['joins'];
+
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select(
-                       [ 'archive', 'revision' ],
+                       $tables,
                        $fields,
                        $oldWhere,
                        __METHOD__,
                        /* options */
                        [ 'ORDER BY' => 'ar_timestamp' ],
-                       [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
+                       $join_conds
                );
 
                $rev_count = $result->numRows();
index edccc66..790845e 100644 (file)
@@ -2484,6 +2484,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $cascade = false;
 
                        if ( $limit['create'] != '' ) {
+                               $commentFields = CommentStore::newKey( 'pt_reason' )->insert( $dbw, $reason );
                                $dbw->replace( 'protected_titles',
                                        [ [ 'pt_namespace', 'pt_title' ] ],
                                        [
@@ -2493,8 +2494,7 @@ class WikiPage implements Page, IDBAccessObject {
                                                'pt_timestamp' => $dbw->timestamp(),
                                                'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
                                                'pt_user' => $user->getId(),
-                                               'pt_reason' => $reason,
-                                       ], __METHOD__
+                                       ] + $commentFields, __METHOD__
                                );
                                $logParamsDetails[] = [
                                        'type' => 'create',
@@ -2746,7 +2746,7 @@ class WikiPage implements Page, IDBAccessObject {
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
                $tags = [], $logsubtype = 'delete'
        ) {
-               global $wgUser, $wgContentHandlerUseDB;
+               global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage;
 
                wfDebug( __METHOD__ . "\n" );
 
@@ -2810,6 +2810,9 @@ class WikiPage implements Page, IDBAccessObject {
                        $content = null;
                }
 
+               $revCommentStore = new CommentStore( 'rev_comment' );
+               $arCommentStore = new CommentStore( 'ar_comment' );
+
                $fields = Revision::selectFields();
                $bitfield = false;
 
@@ -2827,20 +2830,23 @@ class WikiPage implements Page, IDBAccessObject {
                // the rev_deleted field, which is reserved for this purpose.
 
                // Get all of the page revisions
+               $commentQuery = $revCommentStore->getJoin();
                $res = $dbw->select(
-                       'revision',
-                       $fields,
+                       [ 'revision' ] + $commentQuery['tables'],
+                       $fields + $commentQuery['fields'],
                        [ 'rev_page' => $id ],
                        __METHOD__,
-                       'FOR UPDATE'
+                       'FOR UPDATE',
+                       $commentQuery['joins']
                );
                // Build their equivalent archive rows
                $rowsInsert = [];
+               $revids = [];
                foreach ( $res as $row ) {
+                       $comment = $revCommentStore->getComment( $row );
                        $rowInsert = [
                                'ar_namespace'  => $namespace,
                                'ar_title'      => $dbKey,
-                               'ar_comment'    => $row->rev_comment,
                                'ar_user'       => $row->rev_user,
                                'ar_user_text'  => $row->rev_user_text,
                                'ar_timestamp'  => $row->rev_timestamp,
@@ -2854,12 +2860,13 @@ class WikiPage implements Page, IDBAccessObject {
                                'ar_page_id'    => $id,
                                'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
-                       ];
+                       ] + $arCommentStore->insert( $dbw, $comment );
                        if ( $wgContentHandlerUseDB ) {
                                $rowInsert['ar_content_model'] = $row->rev_content_model;
                                $rowInsert['ar_content_format'] = $row->rev_content_format;
                        }
                        $rowsInsert[] = $rowInsert;
+                       $revids[] = $row->rev_id;
                }
                // Copy them into the archive table
                $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
@@ -2874,6 +2881,9 @@ class WikiPage implements Page, IDBAccessObject {
                // Now that it's safely backed up, delete it
                $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
                $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
+               if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
+               }
 
                // Log the deletion, if the page was suppressed, put it in the suppression log instead
                $logtype = $suppress ? 'suppress' : 'delete';
index ddea695..10ba83f 100644 (file)
@@ -89,7 +89,9 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
                        ) );
                        $flag = $attribs['rc_log_action'];
                } else {
-                       $comment = self::cleanupForIRC( $attribs['rc_comment'] );
+                       $comment = self::cleanupForIRC(
+                               CommentStore::newKey( 'rc_comment' )->getComment( $attribs )->text
+                       );
                        $flag = '';
                        if ( !$attribs['rc_patrolled']
                                && ( $wgUseRCPatrol || $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol )
index 047d6cf..998c695 100644 (file)
@@ -102,8 +102,9 @@ class RevDelLogItem extends RevDelItem {
                // User links and action text
                $action = $formatter->getActionText();
                // Comment
+               $comment = CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text;
                $comment = $this->list->getLanguage()->getDirMark()
-                       . Linker::commentBlock( $this->row->log_comment );
+                       . Linker::commentBlock( $comment );
 
                if ( LogEventsList::isDeleted( $this->row, LogPage::DELETED_COMMENT ) ) {
                        $comment = '<span class="history-deleted">' . $comment . '</span>';
@@ -135,7 +136,7 @@ class RevDelLogItem extends RevDelItem {
                }
                if ( LogEventsList::userCan( $this->row, LogPage::DELETED_COMMENT, $user ) ) {
                        $ret += [
-                               'comment' => $this->row->log_comment,
+                               'comment' => CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text,
                        ];
                }
 
index 1932778..ceb97e4 100644 (file)
@@ -63,7 +63,11 @@ class RevDelLogList extends RevDelList {
        public function doQuery( $db ) {
                $ids = array_map( 'intval', $this->ids );
 
-               return $db->select( 'logging', [
+               $commentQuery = CommentStore::getKey( 'log_comment' )->getJoin();
+
+               return $db->select(
+                       [ 'logging' ] + $commentQuery['tables'],
+                       [
                                'log_id',
                                'log_type',
                                'log_action',
@@ -73,13 +77,13 @@ class RevDelLogList extends RevDelList {
                                'log_namespace',
                                'log_title',
                                'log_page',
-                               'log_comment',
                                'log_params',
                                'log_deleted'
-                       ],
+                       ] + $commentQuery['fields'],
                        [ 'log_id' => $ids ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'log_id DESC' ]
+                       [ 'ORDER BY' => 'log_id DESC' ],
+                       $commentQuery['joins']
                );
        }
 
index ede4898..61590d7 100644 (file)
@@ -299,7 +299,7 @@ class SpecialNewpages extends IncludableSpecialPage {
         */
        protected function revisionFromRcResult( stdClass $result ) {
                return new Revision( [
-                       'comment' => $result->rc_comment,
+                       'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
                        'deleted' => $result->rc_deleted,
                        'user_text' => $result->rc_user_text,
                        'user' => $result->rc_user,
index d6eac32..6ee697e 100644 (file)
@@ -636,7 +636,21 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
 
                $message = $this->msg( 'recentchangestext' )->inContentLanguage();
                if ( !$message->isDisabled() ) {
-                       $content = $message->parse();
+                       // Parse the message in this weird ugly way to preserve the ability to include interlanguage
+                       // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use
+                       // $message->parse() instead. This code is copied from Message::parseText().
+                       $parserOutput = MessageCache::singleton()->parse(
+                               $message->plain(),
+                               $this->getPageTitle(),
+                               /*linestart*/true,
+                               // Message class sets the interface flag to false when parsing in a language different than
+                               // user language, and this is wiki content language
+                               /*interface*/false,
+                               $wgContLang
+                       );
+                       $content = $parserOutput->getText();
+                       // Add only metadata here (including the language links), text is added below
+                       $this->getOutput()->addParserOutputMetadata( $parserOutput );
 
                        $langAttributes = [
                                'lang' => $wgContLang->getHtmlCode(),
index 94b36b9..0dd66b0 100644 (file)
@@ -151,7 +151,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                [
                                        'name' => 'extended',
                                        'isReplacedInStructuredUi' => true,
-                                       'isVisible' => false,
                                        'activeValue' => false,
                                        'default' => $this->getUser()->getBoolOption( 'extendwatchlist' ),
                                        'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables,
@@ -334,8 +333,9 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        // unchecked boxes.
                        foreach ( $this->filterGroups as $filterGroup ) {
                                if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
+                                       /** @var ChangesListBooleanFilter $filter */
                                        foreach ( $filterGroup->getFilters() as $filter ) {
-                                               if ( $filter->isVisible() ) {
+                                               if ( $filter->displaysOnUnstructuredUi() ) {
                                                        $allBooleansFalse[$filter->getName()] = false;
                                                }
                                        }
index 51e446d..2206be8 100644 (file)
@@ -173,6 +173,7 @@ class BlockListPager extends TablePager {
                                break;
 
                        case 'ipb_reason':
+                               $value = CommentStore::newKey( 'ipb_reason' )->getComment( $row )->text;
                                $formatted = Linker::formatComment( $value );
                                break;
 
@@ -208,8 +209,10 @@ class BlockListPager extends TablePager {
        }
 
        function getQueryInfo() {
+               $commentQuery = CommentStore::newKey( 'ipb_reason' )->getJoin();
+
                $info = [
-                       'tables' => [ 'ipblocks', 'user' ],
+                       'tables' => [ 'ipblocks', 'user' ] + $commentQuery['tables'],
                        'fields' => [
                                'ipb_id',
                                'ipb_address',
@@ -217,7 +220,6 @@ class BlockListPager extends TablePager {
                                'ipb_by',
                                'ipb_by_text',
                                'by_user_name' => 'user_name',
-                               'ipb_reason',
                                'ipb_timestamp',
                                'ipb_auto',
                                'ipb_anon_only',
@@ -229,9 +231,9 @@ class BlockListPager extends TablePager {
                                'ipb_deleted',
                                'ipb_block_email',
                                'ipb_allow_usertalk',
-                       ],
+                       ] + $commentQuery['fields'],
                        'conds' => $this->conds,
-                       'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ]
+                       'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ] + $commentQuery['joins']
                ];
 
                # Filter out any expired blocks
index 43d7ad4..38a332e 100644 (file)
@@ -69,14 +69,17 @@ class DeletedContribsPager extends IndexPager {
                                ' != ' . Revision::SUPPRESSED_USER;
                }
 
+               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+
                return [
-                       'tables' => [ 'archive' ],
+                       'tables' => [ 'archive' ] + $commentQuery['tables'],
                        'fields' => [
-                               'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment',
+                               'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp',
                                'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted'
-                       ],
+                       ] + $commentQuery['fields'],
                        'conds' => $conds,
-                       'options' => [ 'USE INDEX' => $index ]
+                       'options' => [ 'USE INDEX' => [ 'archive' => $index ] ],
+                       'join_conds' => $commentQuery['joins'],
                ];
        }
 
@@ -253,7 +256,7 @@ class DeletedContribsPager extends IndexPager {
                $rev = new Revision( [
                        'title' => $page,
                        'id' => $row->ar_rev_id,
-                       'comment' => $row->ar_comment,
+                       'comment' => CommentStore::newKey( 'ar_comment' )->getComment( $row )->text,
                        'user' => $row->ar_user,
                        'user_text' => $row->ar_user_text,
                        'timestamp' => $row->ar_timestamp,
index 47b059b..813d1d4 100644 (file)
@@ -244,7 +244,9 @@ class ImageListPager extends TablePager {
                $prefix = $table === 'oldimage' ? 'oi' : 'img';
 
                $tables = [ $table ];
-               $fields = array_keys( $this->getFieldNames() );
+               $fields = $this->getFieldNames();
+               unset( $fields['img_description'] );
+               $fields = array_keys( $fields );
 
                if ( $table === 'oldimage' ) {
                        foreach ( $fields as $id => &$field ) {
@@ -264,6 +266,13 @@ class ImageListPager extends TablePager {
 
                $options = $join_conds = [];
 
+               # Description field
+               $commentQuery = CommentStore::newKey( $prefix . '_description' )->getJoin();
+               $tables += $commentQuery['tables'];
+               $fields += $commentQuery['fields'];
+               $join_conds += $commentQuery['joins'];
+               $fields['description_field'] = "'{$prefix}_description'";
+
                # Depends on $wgMiserMode
                # Will also not happen if mShowAll is true.
                if ( isset( $this->mFieldNames['count'] ) ) {
@@ -497,6 +506,8 @@ class ImageListPager extends TablePager {
                        case 'img_size':
                                return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
                        case 'img_description':
+                               $field = $this->mCurrentRow->description_field;
+                               $value = CommentStore::newKey( $field )->getComment( $this->mCurrentRow )->text;
                                return Linker::formatComment( $value );
                        case 'count':
                                return $this->getLanguage()->formatNum( intval( $value ) + 1 );
index dafd244..53362d9 100644 (file)
@@ -90,15 +90,17 @@ class NewPagesPager extends ReverseChronologicalPager {
                        $conds['page_is_redirect'] = 0;
                }
 
+               $commentQuery = CommentStore::newKey( 'rc_comment' )->getJoin();
+
                // Allow changes to the New Pages query
-               $tables = [ 'recentchanges', 'page' ];
+               $tables = [ 'recentchanges', 'page' ] + $commentQuery['tables'];
                $fields = [
                        'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text',
-                       'rc_comment', 'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted',
+                       'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted',
                        'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid',
                        'page_namespace', 'page_title'
-               ];
-               $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ];
+               ] + $commentQuery['fields'];
+               $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $commentQuery['joins'];
 
                // Avoid PHP 7.1 warning from passing $this by reference
                $pager = $this;
index 823b5da..20b44d2 100644 (file)
@@ -236,6 +236,7 @@ class ProtectedPagesPager extends TablePager {
                                                LogPage::DELETED_COMMENT,
                                                $this->getUser()
                                        ) ) {
+                                               $value = CommentStore::newKey( 'log_comment' )->getComment( $row )->text;
                                                $formatted = Linker::formatComment( $value !== null ? $value : '' );
                                        } else {
                                                $formatted = $this->msg( 'rev-deleted-comment' )->escaped();
@@ -284,8 +285,10 @@ class ProtectedPagesPager extends TablePager {
                        $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
                }
 
+               $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+
                return [
-                       'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ],
+                       'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ] + $commentQuery['tables'],
                        'fields' => [
                                'pr_id',
                                'page_namespace',
@@ -297,9 +300,8 @@ class ProtectedPagesPager extends TablePager {
                                'pr_cascade',
                                'log_timestamp',
                                'log_user',
-                               'log_comment',
                                'log_deleted',
-                       ],
+                       ] + $commentQuery['fields'],
                        'conds' => $conds,
                        'join_conds' => [
                                'log_search' => [
@@ -312,7 +314,7 @@ class ProtectedPagesPager extends TablePager {
                                                'ls_log_id = log_id'
                                        ]
                                ]
-                       ]
+                       ] + $commentQuery['joins']
                ];
        }
 
diff --git a/languages/i18n/ais.json b/languages/i18n/ais.json
new file mode 100644 (file)
index 0000000..e320fdb
--- /dev/null
@@ -0,0 +1,526 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Akamycoco",
+                       "Benel",
+                       "Bunukwiki",
+                       "Tokoabibi"
+               ]
+       },
+       "sunday": "pilipayan",
+       "monday": "sakacacay a demied nu lipay",
+       "tuesday": "sakatusa a demied nu lipay",
+       "wednesday": "sakatulu a demied nu lipay",
+       "thursday": "sakasepat a demied nu lipay",
+       "friday": "sakalima a demied nu lipay",
+       "saturday": "sakaenem a demied nu lipay",
+       "sun": "pilipayan",
+       "mon": "sakacacay a demied nu lipay",
+       "tue": "sakatusa a demied nu lipay",
+       "wed": "sakatulu a demied nu lipay",
+       "thu": "sakasepat a demied nu lipay",
+       "fri": "sakalima a demied nu lipay",
+       "sat": "sakaenem a demied nu lipay",
+       "january": "cacay bulad",
+       "february": "tusa bulad",
+       "march": "tulu bulad",
+       "april": "sepat bulad",
+       "may_long": "lima bulad",
+       "june": "enem bulad",
+       "july": "pitu bulad",
+       "august": "walu bulad",
+       "september": "siwa bulad",
+       "october": "cacay bataan bulad",
+       "november": "sabaw cacay bulad",
+       "december": "sabaw tusa bulad",
+       "january-gen": "cacay bulad",
+       "february-gen": "tusa bulad",
+       "march-gen": "tulu bulad",
+       "april-gen": "sepat bulad",
+       "may-gen": "lima bulad",
+       "june-gen": "enem bulad",
+       "july-gen": "pitu bulad",
+       "august-gen": "walu bulad",
+       "september-gen": "siwa bulad",
+       "october-gen": "cacay bataan bulad",
+       "november-gen": "sabaw cacay bulad",
+       "december-gen": "sabaw tusa bulad",
+       "jan": "cacay bulad",
+       "feb": "tusa bulad",
+       "mar": "tulu bulad",
+       "apr": "sepat bulad",
+       "may": "lima bulad",
+       "jun": "enem bulad",
+       "jul": "pitu bulad",
+       "aug": "walu bulad",
+       "sep": "siwa bulad",
+       "oct": "cacay bataan bulad",
+       "nov": "sabaw cacay bulad",
+       "dec": "sabaw tusa bulad",
+       "january-date": "cacay bulad $1",
+       "february-date": "tusa bulad $1",
+       "march-date": "tulu bulad $1",
+       "april-date": "sepat bulad $1",
+       "may-date": "lima bulad $1",
+       "june-date": "enem bulad $1",
+       "july-date": "pitu bulad $1",
+       "august-date": "walu bulad $1",
+       "september-date": "siwa bulad $1",
+       "october-date": "cacay bataan bulad $1",
+       "november-date": "sabaw cacay bulad $1",
+       "december-date": "sabaw tusa bulad $1",
+       "period-am": "AM",
+       "period-pm": "PM",
+       "pagecategories": "{{PLURAL:$1|kakuniza}}",
+       "category_header": "abuay nu kasasizuma \"$1\" a kasabelih",
+       "subcategories": "sailuc-kasasizuma",
+       "category-media-header": "kakuniza labuay \"$1\" a myiti",
+       "hidden-categories": "{{PLURAL:$1|midimut kakuniza}}",
+       "category-subcat-count": "{{PLURAL:$2|uyni kakuniza hatiza ku cacay yamalyilu sailuc-kakuniza. kina kakuniza yamalyilu isasa $2 a sailuc-kasasizuma, ilabu {{PLURAL:$1}}mahiza ku isasaay}}",
+       "category-article-count": "{{PLURAL:$2|uyni kakuniza hatiza ku cacay yamalyilu sailuc-kakuniza. kina kakuniza yamalyilu isasa $2 a sailuc-kasasizuma, ilabu {{PLURAL:$1}}mahiza ku isasaay}}",
+       "category-file-count": "{{PLURAL:$2|kakuniza yamalyilu isasaay a cacay ku tangan. kakuniza yamalyilu isasaay izaway $1 ku tangan, pulung $2 makalaan.}}",
+       "listingcontinuesabbrev": "palalid",
+       "about": "mahizaay u",
+       "newwindow": "(paynin baluhay a azih-sasingalan miwawah)",
+       "cancel": "palawpes",
+       "moredotdotdot": "yadah...",
+       "mypage": "kasabelih",
+       "mytalk": "sasukamu",
+       "anontalk": "sasukamu",
+       "navigation": "pasubana’ tu miidangay",
+       "and": "&#32;",
+       "namespaces": "pangangananay a salaedan",
+       "variants": "masazumaay",
+       "navigation-heading": "pasubana’ tu miidangay pipili’an",
+       "returnto": "tatiku tazuma至 $1.",
+       "tagline": "makayzaay i {{SITENAME}}",
+       "help": "buhci tu kamu",
+       "search": "kilim",
+       "searchbutton": "kilim",
+       "go": "mileku",
+       "searcharticle": "mileku",
+       "history": "kasabelih nazipa’an",
+       "history_short": "nazipa’an",
+       "history_small": "nazipa’an",
+       "printableversion": "kapah tu insace baziyong / sapad",
+       "permalink": "saluimengay misiket",
+       "view": "ciwsace",
+       "view-foreign": "i $1 miciwsace",
+       "edit": "mikawaway tu kalumyiti",
+       "create": "patizeng",
+       "create-local": "cunusen itiniay a buhci tu kamu",
+       "newpage": "baluhayay a kasabelih",
+       "talkpagelinktext": "sasukamu",
+       "personaltools": "teked sakaluk",
+       "talk": "matatengil",
+       "views": "ciwsace",
+       "toolbox": "sakaluk",
+       "otherlanguages": "zumaay a kamu",
+       "redirectedfrom": "(miliyawtu tazuma nay $1)",
+       "redirectpagesub": "miliyaw tazuma kasabelih",
+       "redirectto": "miliyaw tazuma tu:",
+       "lastmodifiedat": "uyni kasabelih sazikuz mikawaway tu kalumyiti i $1 $2.",
+       "jumpto": "taayaw:",
+       "jumptonavigation": "pasubana’ tu miidangay",
+       "jumptosearch": "kilim",
+       "aboutsite": "mahizaay u {{SITENAME}}",
+       "aboutpage": "Project:mahizaay u...",
+       "copyright": "anu izaw ku zuma buhci tu kamu, kasabelih aazihen a lacul i labu, pisaungay hamin $1 sapabeli tu kinli a ceding.",
+       "copyrightpage": "{{ns:project}}:nisanga’an niza tu tungus a kawaw",
+       "currentevents": "ayzaay a sinbun",
+       "currentevents-url": "Project:Current events",
+       "disclaimers": "caay pimuku tu sikining",
+       "disclaimerpage": "Project:habutud a pimuku tu sikining",
+       "edithelp": "mikawaway tu kalumyiti buhci tu kamu",
+       "helppage-top-gethelp": "buhci tu kamu",
+       "mainpage": "saayaway a belih",
+       "mainpage-description": "saayaway a belih",
+       "policy-url": "Project:Policy",
+       "portal": "komiyonityi sacumudan",
+       "portal-url": "Project:komiyonityi sacumudan",
+       "privacy": "salimek a mikuwanay a kawaw",
+       "privacypage": "Project:salimek a mikuwanay a kawaw",
+       "retrievedfrom": "miala i \"$1\"",
+       "editsection": "mikawaway tu kalumyiti",
+       "editold": "mikawaway tu kalumyiti",
+       "viewsourceold": "ciwsace sakatizeng bangu",
+       "editlink": "mikawaway tu kalumyiti",
+       "viewsourcelink": "ciwsace sakatizeng bangu",
+       "editsectionhint": "mikawaway tu kalumyiti tusil: $1",
+       "toc": "dilyikotoling",
+       "showtoc": "paazih",
+       "hidetoc": "midimut",
+       "confirmable-yes": "hang",
+       "confirmable-no": "caay",
+       "site-atom-feed": "$1 a Atom saangangan",
+       "page-atom-feed": "$1 a Atom saangangan",
+       "red-link-title": "$1 (kasabelih nayai’ tu)",
+       "nstab-main": "kasabelih",
+       "nstab-user": "misaungayay a kasabelih",
+       "nstab-special": "sazumaay a kasabelih",
+       "nstab-project": "cwanan kasabelih",
+       "nstab-image": "tangan",
+       "nstab-template": "taazihan mitudung",
+       "nstab-category": "kakuniza",
+       "mainpage-nstab": "saayaway a belih",
+       "badtitle": "a’cusay a pyawti",
+       "badtitletext": "matuzu’ay a kasabelih pyawti u la’cusay、nayi’ ku cacan, caaysa tatenga’ay tu misiket kamu Wikiay a pyawti.\ntebanay pyawti akay amalyilu la’cusay pisaungay i pyawtayi a tatebanan nu nisulitan.",
+       "viewsource": "ciwsace sakatizeng bangu",
+       "userlogin-yourname": "misaungayay a kalungangan",
+       "userlogin-yourname-ph": "pisulitan tu nu misay a misaungayay a kalungangan",
+       "userlogin-yourpassword": "mima",
+       "userlogin-yourpassword-ph": "suliten nu misuay a mima",
+       "createacct-yourpassword-ph": "pisulitan ku mima",
+       "createacct-yourpasswordagain": "malucekay tu mima",
+       "createacct-yourpasswordagain-ph": "pisulitan ku mima kinacacay aca",
+       "userlogin-remembermypassword": "i balucu’en aku patalabu setyitase",
+       "login": "patalabu",
+       "nav-login-createaccount": "patalabu / panganganen ku canghaw",
+       "userlogin-noaccount": "inayi’ ku canghaw kisu haw?",
+       "userlogin-joinproject": "micunus {{SITENAME}}",
+       "createaccount": "panganganen ku canghaw",
+       "userlogin-resetpassword-link": "maliyuh ku mima kisu haw?",
+       "userlogin-helplink2": "patalabu miedap",
+       "createacct-emailoptional": "imyiyo(email) tigami (u pili’ay sasulitan)",
+       "createacct-email-ph": "pisulitan ku imyiyo(email) nu misu",
+       "createacct-submit": "panganganen ku misuay a canghaw",
+       "createacct-another-submit": "panganganen ku canghaw",
+       "createacct-benefit-heading": "{{SITENAME}} paanin tu nisulitan tu nu tapangay mahiza kisuan.",
+       "createacct-benefit-body1": "saka{{PLURAL:$1|ku mikawaway tu kalumyiti}}",
+       "createacct-benefit-body2": "{{PLURAL:$1| kasabelih}}",
+       "createacct-benefit-body3": "cay katenesay{{PLURAL:$1|paaninay tu kalusasing}}",
+       "loginlanguagelabel": "kamu: $1",
+       "pt-login": "patalabu",
+       "pt-login-button": "patalabu",
+       "pt-createaccount": "panganganen ku canghaw",
+       "pt-userlogout": "katahkal",
+       "botpasswords-label-cancel": "palawpes",
+       "resetpass-submit-cancel": "palawpes",
+       "passwordreset": "miliyaw miteka setin mima",
+       "bold_sample": "kibetulay a sulit",
+       "bold_tip": "kibetulay a sulit",
+       "italic_sample": "tukenihay nisulit",
+       "italic_tip": "tukenihay nisulit",
+       "link_sample": "misiket satangahan a sulit",
+       "link_tip": "labu-labu misiket",
+       "extlink_sample": "http://www.example.com misiket satangahan a sulit",
+       "extlink_tip": "hekal-hekal misiket (amana katawal saka http:// paangangan)",
+       "headline_sample": "sakacacay a selal nu satangahan a sulit",
+       "headline_tip": "sakatusa a selal nu satangahan a sulit",
+       "nowiki_sample": "pacucuk caayay kesehwaay a cudad",
+       "nowiki_tip": "sekipo Wiki kesehwa sulit nu kamu",
+       "image_tip": "nicunusay a tangan",
+       "media_tip": "tangan-tangan misiket",
+       "sig_tip": "misuay a sulit nu ngangan atu demiad, tuki",
+       "hr_tip": "Sapisasuala (cayka yadah kawiza)",
+       "summary": "pecu’ nu lacul:",
+       "minoredit": "payni mikilulay a mikawaway tu kalumyiti",
+       "watchthis": "miazih tuyni kasabelih",
+       "savearticle": "misuped kasabelih",
+       "showpreview": "paazih pataayaway miazih",
+       "showdiff": "paazih ku masumaday",
+       "anoneditwarning": "<strong>patalaw:</strong>caay henay kisu patalabu. anu miteka mikawaway tu kalumyiti, IP adolyise nu misu ama mitilak. anu kisu <strong>[$1  patalabu ]</strong> acasa <strong>[$2 panganganen ku canghaw ]</strong>, misuay mikawaway tu kalumyiti payni tu nu misuay misaungayay kalungangan sacuzu’ ,izaway zuma kapahayay.",
+       "loginreqlink": "patalabu",
+       "newarticletext": "masasiket kisu tu nayi’ay tu kasabelih.\namipatizeng tina kasabelih, kaisasa mikawaway tu kalumyiti atilad misulit ku lacul (kahica nu kawaw piazih tu tatenga’ay [$1 misaungay a buhci tu kamu  kasabelih ]).\namahica caay padeteng tayza tina kasabelih kisu haw, pihaymaw sapecec saazihay a <strong>tatiku</strong> pipenecan.",
+       "noarticletext": "kina kasabelih inayi’ lacul ayza,kapah tu kisu i zumaay a kasabelih [[Special:Search/{{PAGENAME}}| mikilim kina kasabelih pyawti ]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  mikilim sasuala nasulitan nakawawan ] caay sa[{{fullurl:{{FULLPAGENAME}}|action=edit}} patizeng kina kasabelih ]</span>.",
+       "noarticletext-nopermission": "tina kasabelih ayza inayi’ lacul,\nkapah tu kisu i zuma kasabelih [[Special:Search/{{PAGENAME}}| kilim kina kasabelih pyawti ]],acasa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  kilim sasuala nasulitan nakawawan ]</span>,uyzasa  inayi’  ku tungus patizeng tina kasabelih.",
+       "editing": "mikawaway tu kalumyiti  $1 ayza",
+       "creating": "patizeng ayza $1",
+       "editingsection": "mikawaway tu kalumyiti ayza $1 (tusil)",
+       "templatesused": "uyni kasabelih pisaungay tu isasaay {{PLURAL:$1|taazihan mitudung}}:",
+       "template-protected": "(madiputay)",
+       "template-semiprotected": "(madiputay a kasabelih - satizep mikawaway tu kalumyiti)",
+       "hiddencategories": "kina kasabelih tungusay nu {{PLURAL:$1|1 midimut kakuniza }}mamikawaw:",
+       "permissionserrorstext-withaction": "namakay isasaay {{PLURAL:$1|mahicaay}}, inayi’ kisu situngus miteka $2 miteka tuway misaungay:",
+       "moveddeleted-notice": "kina kasabelih masipu tu.\nisasa nipabeli kina kasabelihay a masipu atu milimad nasulitan nakawawan, taneng miazih tu tatenga’ay.",
+       "viewpagelogs": "ciwsace kina kasabelih a nasulitan nakawawan",
+       "currentrev-asof": "i $1 a sabaluhay masumad",
+       "revisionasof": "$1 a sumad",
+       "revision-info": "i $1 tuyni a {{GENDER:$6|$2}} u nasumad nu nikawawan",
+       "previousrevision": "← masumad nu ayaway",
+       "nextrevision": "kilulay masumad →",
+       "currentrevisionlink": "sabaluhay masumad",
+       "cur": "ayza",
+       "last": "ayaway",
+       "revdelete-show-file-submit": "hang",
+       "history-title": "\"$1\" masumaday a nazipa’an",
+       "difference-title": "\"$1\" misumad laeday sasizuma",
+       "lineno": "silsil $1:",
+       "editundo": "patiku",
+       "diff-multi-sameuser": "(malecaday misaungayay {{PLURAL:$1| ilaed izaw ku $1 a sumad}}inayi’ paazih)",
+       "searchresults": "heci nu makatepa",
+       "searchresults-title": "$1 heci nu makatepa",
+       "prevn": "ayaw saka {{PLURAL:$1|$1}}",
+       "nextn": "zikuzan saka {{PLURAL:$1|$1}}",
+       "nextn-title": "nuzikuzan saka {{PLURAL:$1|a heci}}",
+       "shown-title": "paybelih {{PLURAL:$1|$1 ku heci}} paazih",
+       "viewprevnext": "ciwsace ($1 {{int:pipe-separator}} $2) ($3)",
+       "searchmenu-new": "<strong> uyni Wiki patizeng ku kasabelih  \"[[:$1]]\"!</strong>{{PLURAL:$2|0=| acasa miazih tu tatenga’ay patalabuay a matepa’ay a heci. |acasa miazih tu tatenga’ay zuma matepa’ay a heci.}}",
+       "searchprofile-articles": "lacul kasabelih",
+       "searchprofile-images": "malocimyidiya",
+       "searchprofile-everything": "hamin",
+       "searchprofile-advanced": "tapabaw",
+       "searchprofile-articles-tooltip": "i labu nu $1 mikilim",
+       "searchprofile-images-tooltip": "mikilim tu tangan",
+       "searchprofile-everything-tooltip": "kilim saca hamin lacul (yamalyilu sasukamu kasabelih)",
+       "searchprofile-advanced-tooltip": "mikilim pakuniza misanga’ pangangananay a salaedan",
+       "search-result-size": "$1 ({{PLURAL:$2|$2 ku sulit}})",
+       "search-redirect": "(miliyaw tazuma namakay $1)",
+       "search-section": "(tusil $1)",
+       "search-suggest": "u tuzu’ nu misu ku:$1 haw?",
+       "searchall": "hamin",
+       "search-showingresults": "{{PLURAL:$4|saka <strong>$1</strong> a heci, pulung <strong>$3</strong>|saka <strong>$1-$2</strong> a heci, pulung <strong>$3</strong>}}",
+       "search-nonefound": "nayi’ matatungusay palalitemuh tu kawaw maheciay.",
+       "mypreferences": "setin tu kanamuhan",
+       "prefs-rc": "capi a demaiday a sumad",
+       "searchresultshead": "kilim",
+       "prefs-searchoptions": "kilim",
+       "right-writeapi": "pisaungay suliten API",
+       "grant-createaccount": "panganganen ku canghaw",
+       "newuserlogpage": "patizeng misaungayay nasulitan nakawawan",
+       "action-edit": "mikawaway tu kalumyiti uyni kasabelih",
+       "enhancedrc-history": "nazipa’an",
+       "recentchanges": "capi a demaiday a sumad",
+       "recentchanges-legend": "capi a demiad masumaday a mapiliay",
+       "recentchanges-summary": "mikilul nazikuzan ilabu nu Wikiay a kasabelihay a capi demiad a nasumaday.",
+       "recentchanges-label-newpage": "uyni mikawaway tu kalumyiti patizengtu baluhay kasabelih",
+       "recentchanges-label-minor": "payni mikilulay a mikawaway tu kalumyiti",
+       "recentchanges-label-bot": "uyni mikawaway tu kalumyiti u kikay a tademaw mileku",
+       "recentchanges-label-unpatrolled": "mikawaway tu kalumyiti caay henay ka tayza mikibi",
+       "recentchanges-label-plusminus": "na kasabelih misumad hacica ku tabaki (wyiyincu )",
+       "recentchanges-legend-heading": "<strong>u tinaku nu kulit:</strong>",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (piazih tu tatenga’ay [[Special:NewPages| baluhayay a kasabelih]])",
+       "rcfilters-savedqueries-cancel-label": "palawpes",
+       "rclistfrom": "paazih nay $3 $2 baluhayay a sumad katukuh ayza",
+       "rcshowhideminor": "$1 mikilulay mikawaway tu kalumyiti",
+       "rcshowhideminor-show": "paazih",
+       "rcshowhideminor-hide": "Hide",
+       "rcshowhidebots": "$1 kikay a tademaw",
+       "rcshowhidebots-show": "paazih",
+       "rcshowhidebots-hide": "midimut",
+       "rcshowhideliu": "$1 mapangangan tuway a misaungayay",
+       "rcshowhideliu-show": "paazih",
+       "rcshowhideliu-hide": "midimut",
+       "rcshowhideanons": "$1 paceba panganganay a misaungayay",
+       "rcshowhideanons-show": "paazih",
+       "rcshowhideanons-hide": "midimut",
+       "rcshowhidepatr-hide": "midimut",
+       "rcshowhidemine": "$1 mikawaway tu kalumyiti nu maku",
+       "rcshowhidemine-show": "paazih",
+       "rcshowhidemine-hide": "midimut",
+       "rcshowhidecategorization-hide": "midimut",
+       "rclinks": "paazih capi a demiad $2 a demiaday a saka $1 a sumad.",
+       "diff": "sasizuma",
+       "hist": "nazipa’an",
+       "hide": "midimut",
+       "show": "paazih",
+       "minoreditletter": "adidi’",
+       "newpageletter": "baluhay",
+       "boteditletter": "kikay a tademaw",
+       "rc-change-size-new": "masumadtu sa u $1 {{PLURAL:$1|wyiyincu}}",
+       "recentchangeslinked": "sasuala a sumad",
+       "recentchangeslinked-feed": "sasuala a sumad",
+       "recentchangeslinked-toolbox": "sasuala a sumad",
+       "recentchangeslinked-title": "pulung \"$1\" sasuala a sumaday",
+       "recentchangeslinked-summary": "uyni kasabelih pasilsil micuzu’ kasabelih masasiketay saca hamin kasabelihan (hakya u matuzu’ay kakuniza\nilabu saca hamin mamikawaw)izaw ku  misumaday kasabelih piazihan tu sulit.\nizawtu ku [[Special:Watchlist|sapacukat a sulit nu misu]] ilabuay a kasabelih apatahkal ku <strong>kibetulay a sulit</strong> sacuzu’.",
+       "recentchangeslinked-page": "kasabelih kalungangan:",
+       "recentchangeslinked-to": "Show changes to pages linked to the given page instead\nmisumad ku paazih masasiket tayza matuzu’ay kasabelih a nisumad",
+       "upload": "patapabaw ku tangan",
+       "filedesc": "pecu’ nu lacul",
+       "upload-dialog-button-cancel": "palawpes",
+       "license": "sapabeli tu kinli a cedang",
+       "license-header": "sapabeli tu kinli a cedang",
+       "imgfile": "tangan",
+       "listfiles": "tangan-tangan misiket",
+       "listfiles-latestversion-yes": "hang",
+       "listfiles-latestversion-no": "caay",
+       "file-anchor-link": "tangan",
+       "filehist": "tangan nu nazipa’an",
+       "filehist-help": "sapecec ku demiad/tuki sapiciwsace ku tuki nina demiad a tangan baziyong",
+       "filehist-current": "ayza",
+       "filehist-datetime": "demiad/tuki",
+       "filehist-thumb": "sukep tu zunga",
+       "filehist-thumbtext": "nu $1 baziyongay a sukep tu zunga",
+       "filehist-user": "misaungayay",
+       "filehist-dimensions": "ditek",
+       "filehist-comment": "pacunus sakacaay kapawan",
+       "imagelinks": "tangan sadama tu kawaw",
+       "linkstoimage": "isasaay {{PLURAL:$1| kasabelih  misiket |saka $1 a kasabelih misiket}}katukuh tina tangan:",
+       "nolinkstoimage": "nayi’ ku kasabelih masasiket katukuh tini a tangan.",
+       "sharedupload-desc-here": "kina tangan nay $1 hakay satu pisaungay tu zuma a cwanan.\nisasaay paazih kuyniay a tangan i [$2 tangan patahkal kasabelih] a patahkalay a lacul.",
+       "upload-disallowed-here": "la’cus kisu mitahpu tuyni a tangan.",
+       "randompage": "kakibalucu’ ay a kasabelih",
+       "randomincategory-submit": "mileku",
+       "pageswithprop-submit": "mileku",
+       "nbytes": "$1 {{PLURAL:$1|wyiyincu}}",
+       "ncategories": "{{PLURAL:$1|kakuniza}}",
+       "nmembers": "$1 {{PLURAL:$1|ku mamikawaw}}",
+       "newpages": "baluhay kasabelih",
+       "move": "milimad",
+       "pager-older-n": "{{PLURAL:$1| kusa malumanay}}",
+       "apisandbox-continue": "palalid",
+       "booksources": "nu cudad atu laculaculan",
+       "booksources-search-legend": "mikilim ku cudad atu laculaculan",
+       "booksources-search": "kilim",
+       "log": "nasulitan nakawawan",
+       "allpages": "hamin nu kasabelih",
+       "allarticles": "hamin nu kasabelih",
+       "allpagessubmit": "mileku",
+       "categories": "kakuniza",
+       "linksearch-ok": "kilim",
+       "watchlist": "miazihay a piazihan tu sulit",
+       "mywatchlist": "miazihay a piazihan tu sulit",
+       "watch": "miazih",
+       "watchlist-hide": "midimut",
+       "dellogpage": "masipu ku nasulitan nakawawan",
+       "rollbacklink": "panukasan",
+       "rollbacklinkcount": "patiku {{PLURAL:$1|mikawaway tu kalumyiti}}",
+       "protectlogpage": "midiput nasulitan nakawawan",
+       "restriction-edit": "mikawaway tu kalumyiti",
+       "restriction-move": "milimad",
+       "undelete-search-submit": "kilim",
+       "undelete-show-file-submit": "hang",
+       "namespace": "pangangananay a salaedan:",
+       "invert": "kabelihan mipili’",
+       "tooltip-invert": "pili’en hatizaay kuyni mapili’ay atilad i midimut mipili’panganganan a salaedan blabuay kasabelih sumad (anu hatizaay sasuala panganganan a salaedan, mama palecad midimut sasuala panganganan a salaedan)",
+       "namespace_association": "sasuala panganganan a salaedan",
+       "tooltip-namespace_association": "pili’en hatizaay kuyni mapili’ay atilad i yamalyilu atu mipili’ sasuala panganganan a salaedan sasukamu atu satangahan panganganan a salaedan",
+       "blanknamespace": "(sausi)",
+       "contributions": "{{GENDER:$1| misaungayay}}paanin tu nisulitan",
+       "mycontris": "paanin",
+       "anoncontribs": "paanin",
+       "uctop": "(ayza)",
+       "month": "sazikuzay demiad nabuladan:",
+       "year": "sazikuzay demiad mihcaan:",
+       "sp-contributions-blocklog": "milangat tu nasulitan nakawawan",
+       "sp-contributions-logs": "nasulitan nakawawan",
+       "sp-contributions-talk": "sasukamu",
+       "sp-contributions-submit": "kilim",
+       "whatlinkshere": "masasiket katukuh uyniyay a kasabelih",
+       "whatlinkshere-title": "masasiket tazumaay a \"$1\" kasabelih",
+       "whatlinkshere-page": "Kasabelih:",
+       "linkshere": "isasaay a kasabelih masasiket tazuma tu <strong>[[:$1]]</strong>:",
+       "isredirect": "miliyaw tazuma kasabelih",
+       "istemplate": "nicaliwan",
+       "isimage": "tangan-tangan misiket",
+       "whatlinkshere-prev": "saka {{PLURAL:$1|nuayaway}}",
+       "whatlinkshere-next": "saka {{PLURAL:$1|nuzikuzan}}",
+       "whatlinkshere-links": "← masasiket",
+       "whatlinkshere-hideredirs": "$1 miliyaw tazuma",
+       "whatlinkshere-hidetrans": "$1 nicaliwan",
+       "whatlinkshere-hidelinks": "$1 masasiket",
+       "whatlinkshere-hideimages": "$1 tangan-tangan misiket",
+       "whatlinkshere-filters": "kilim",
+       "whatlinkshere-submit": "mileku",
+       "autoblocklist-submit": "kilim",
+       "ipblocklist-submit": "kilim",
+       "blocklink": "malangat",
+       "contribslink": "paanin",
+       "blocklogpage": "milangat tu nasulitan nakawawan",
+       "movelogpage": "milimad ku nasulitan nakawawan",
+       "export": "patahkal ku kasabelih",
+       "allmessages-filter-submit": "mileku",
+       "thumbnail-more": "patabaki(micuwat)",
+       "tooltip-pt-userpage": "{{GENDER:|misaungayay nu misu}} kasabelih",
+       "tooltip-pt-mytalk": "{{GENDER:|misuay }}sasukamu a kasabelih",
+       "tooltip-pt-preferences": "{{GENDER:|misuay}} setin tu kanamuhan",
+       "tooltip-pt-watchlist": "miazih kisu misumad kasabelih piazihan tu sulit ayza",
+       "tooltip-pt-mycontris": "{{GENDER:|misuay}}paaninay a piazihan tu sulit",
+       "tooltip-pt-login": "patahkal nizateng kisu ayaw patalabu, uyza sa kapah tu amana.",
+       "tooltip-pt-logout": "katahkal",
+       "tooltip-pt-createaccount": "taneng kami pauuh kisuan panganganen tu cacay canghaw atu patalabu, anu caay ku tabakiay a pisaungay.",
+       "tooltip-ca-talk": "matatengil tu mahizaay lacul nu kasabelih",
+       "tooltip-ca-edit": "mikawaway tu kalumyiti uyni kasabelih",
+       "tooltip-ca-viewsource": "uyni kasabelih madiputay tuway.\nkapah kisu miciwsace tuyni kasabelih sakatizeng bangu",
+       "tooltip-ca-history": "uyini kasabelih nasawniay a sumad",
+       "tooltip-ca-move": "milimad tina kasabelih",
+       "tooltip-ca-watch": "paynien kasabelih micunus misuay cyinse piazihan tu sulit",
+       "tooltip-search": "kilim {{SITENAME}}",
+       "tooltip-search-go": "amahica milihiza tuyni kalungangan malalanepay kasabelih izaay tu, taayawen ku tiza kasabelih",
+       "tooltip-search-fulltext": "mikilim pisaungay tina paycudadcudad a kasabelih",
+       "tooltip-p-logo": "taayaw saayaway a belih",
+       "tooltip-n-mainpage": "taayaw saayaway a belih",
+       "tooltip-n-mainpage-description": "taayaw saayaway a belih",
+       "tooltip-n-portal": "mahizaay uyni a cwanan, amihica kisu kapah tu、icuwa amatepa maydihay a kawaw atu duut nu misu",
+       "tooltip-n-currentevents": "i labu nu sinbun a kawaw, matepa sasuala tada kalunasulitan",
+       "tooltip-n-recentchanges": "pasilsil tuyni Wiki labuay a sapisumaday a piazihan tu sulit",
+       "tooltip-n-randompage": "kakibalucu’ ay micumud cacay a kasabelih",
+       "tooltip-n-help": "mamiedapay a kakitizaan",
+       "tooltip-t-whatlinkshere": "pasilsil saca hamin masasiket uyni kasabelihay a kasabelih",
+       "tooltip-t-recentchangeslinked": "uyni kasabelih masasiket tayza i zuma  kasabelih capi demiaday a sumad",
+       "tooltip-feed-atom": "uyni a kasabelih nu Atom saanganga",
+       "tooltip-t-contributions": "{{GENDER:$1| tina misaungayay}} a paanin tu nisulitan piazihan tu sulit",
+       "tooltip-t-upload": "patapabaw ku tangan",
+       "tooltip-t-specialpages": "hamin sazumaay kasabelih piazihan tu sulit",
+       "tooltip-t-print": "uyni kasabelihay a taneng insace a baziyong",
+       "tooltip-t-permalink": "uyni kasabelih masumaday saluimengay misiket",
+       "tooltip-ca-nstab-main": "ciwsace kasabelihay a lacul",
+       "tooltip-ca-nstab-user": "ciwsace misaungayay a kasabelih",
+       "tooltip-ca-nstab-special": "uyni kasabelih u sazumaay belih, cayka tineng mikawaway tu kalumyiti",
+       "tooltip-ca-nstab-project": "ciwsace cwanan kasabelih",
+       "tooltip-ca-nstab-image": "ciwsace tangan kasabelih",
+       "tooltip-ca-nstab-template": "ciwsace taazihan mitudung",
+       "tooltip-ca-nstab-category": "ciwsace kakuniza a kasabelih",
+       "tooltip-save": "misuped misuay a pisumad",
+       "tooltip-preview": "kay iayaw nu pisuped miazih tu nu misuay nisumad.",
+       "tooltip-diff": "paazih hica sa kisu labuay a nisumadan",
+       "tooltip-watch": "paynien kasabelih micunus misuay cyinse piazihan tu sulit",
+       "tooltip-rollback": "sapecec \"patiku\" misiket, kapah patiku tayza nuayawanay a paanin tu nisulitan tina kasabelih mikawaway tu kalumyiti",
+       "tooltip-undo": "\"patiku\" kapah tu patiku tuyni mikawaway tu kalumyiti payni pataayaway miazih muse miwawah mikawaway tu kalumyiti aazihan cudad, saka caay cayaw nu labu micunus mahicaay.",
+       "tooltip-summary": "kapisulitan apuyu’ay a pecu’ nu lacul",
+       "simpleantispam-label": "sapi tena’ babakahen a sulit kinsa.\nyu <strong>amana</strong> misulit kuyni pisinga’an!",
+       "pageinfo-toolboxlink": "kasabelih cesyun",
+       "pageinfo-contentpage-yes": "hang",
+       "pageinfo-protect-cascading-yes": "hang",
+       "previousdiff": "← malumanay a mikawaway tu kalumyiti",
+       "nextdiff": "baluhayay mikawaway tu kalumyit →",
+       "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|ku kasabelih}}",
+       "file-info-size": "$1 × $2 syangsu, hacica ku tabaki nu tangan: $3, MIME kakuniza: $4",
+       "file-info-size-pages": "$1 × $2 syangsu, hacica ku tabaki nu tangan:$3,MIME kakuniza: $4, $5 {{PLURAL:$5|ku kasabelih}}",
+       "file-nohires": "inay ku sangaleb takalaway a katingalaw, kapah tu nipabeli.",
+       "svg-long-desc": "SVG tangan, maazihay hacica ku tabaki $1 × $2  syangsu, tangan hacica ku tabaki: $3",
+       "show-big-image": "saayaway a tangan",
+       "show-big-image-preview": "pataayaway miazih hacica ku tabaki: $1.",
+       "show-big-image-size": "$1 × $2 syangsu",
+       "ilsubmit": "kilim",
+       "metadata": "pulita tu kalunasulitan",
+       "metadata-help": "uyni tangan labuay amin yamalyilu zuma cesyun, uyni a cesyun akay nay suewyi  sasasing asaca sapisekyin i tapang asaca suwyihwa saayaw katukuh pahzekan a nakawawan mapacunusay. anu nay tangan saayaway setyitase masumadtu, hatizaay pulita kalunasulitan akay la’cus mileku mabetil a misumad tu tangan.",
+       "metadata-fields": "i tini palatuh patahkal i labuay a EXIF pulita tu kalunasulitanay a kakitizaan, yamalyilu i zunga paazih kasabelih, sapipulita tu cudad nu nasulitan malepi’ paazih palatuh.\nzumaay a pulita tu cudad pataayaw tu kawaw midimut.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
+       "exif-orientation": "papayzaan",
+       "exif-xresolution": "sasuala katingalaw",
+       "exif-yresolution": "mitelekay katingalaw",
+       "exif-datetime": "tangan misumaday a demiad atu tuki",
+       "exif-make": "sasasing misanga’ay tu kalutuud",
+       "exif-model": "sasasing silosi nu nisanga’an",
+       "exif-software": "pisaungay zwanti",
+       "exif-exifversion": "Exif baziyong / sapad",
+       "exif-colorspace": "kalukulit salaedan",
+       "exif-datetimeoriginal": "kalunasulitan masangaay’ a demiad atu tuki",
+       "exif-datetimedigitized": "suwyihwaay a demiad atu tuki",
+       "exif-orientation-1": "tatungus",
+       "namespacesall": "hamin",
+       "monthsall": "hamin",
+       "imgmultigo": "mileku!",
+       "img-lang-go": "mileku",
+       "table_pager_limit_submit": "mileku",
+       "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1| sasukamu ]])",
+       "version-specialpages": "sazumaay a kasabelih",
+       "redirect-submit": "mileku",
+       "fileduplicatesearch-submit": "kilim",
+       "specialpages": "sazumaay a kasabelih",
+       "specialpages-group-login": "patalabu / panganganen ku canghaw",
+       "tag-filter": "[[Special:Tags|aazihen paya]] kilim:",
+       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|aazihen paya’}}]]: $2)",
+       "tags-active-yes": "hang",
+       "tags-active-no": "caay",
+       "permanentlink": "saluimengay misiket",
+       "htmlform-no": "caay",
+       "htmlform-yes": "hang",
+       "logentry-delete-delete": "$1 {{GENDER:$2|masiputu}} kasabelih $3",
+       "restore-count-files": "{{PLURAL:$1|1 tangan}}",
+       "logentry-move-move": "$1 {{GENDER:$2|milimad tuway}} kasabelih $3 katukuh $4",
+       "logentry-newusers-create": "{{GENDER:$2|patizeng}} misaungayay canghaw tuway $1",
+       "logentry-upload-upload": "$1 {{GENDER:$2|masayacay tuway}} $3",
+       "feedback-cancel": "palawpes",
+       "searchsuggest-search": "kilim {{SITENAME}}"
+}
index 18ccaa1..651d491 100644 (file)
        "redirect-lookup": "Шукаць паводле:",
        "redirect-value": "Значэньне:",
        "redirect-user": "Ідэнтыфікатару ўдзельніка",
-       "redirect-page": "Ідэнтыфікатар старонкі",
+       "redirect-page": "Ідэнтыфікатару старонкі",
        "redirect-revision": "Вэрсіі старонкі",
        "redirect-file": "Імя файла",
        "redirect-logid": "ID журнала",
        "fileduplicatesearch-noresults": "Файл з назвай «$1» ня знойдзены.",
        "specialpages": "Спэцыяльныя старонкі",
        "specialpages-note-top": "Легенда",
+       "specialpages-note-restricted": "* Звычайныя спэцыяльныя старонкі.\n* <span class=\"mw-specialpagerestricted\">Спэцыяльныя старонкі з абмежаваным доступам.</span>",
        "specialpages-group-maintenance": "Тэхнічныя справаздачы",
        "specialpages-group-other": "Іншыя спэцыяльныя старонкі",
        "specialpages-group-login": "Уваход / стварэньне рахунку",
index efa97ee..c8990bd 100644 (file)
@@ -39,7 +39,7 @@
        "tog-enotifminoredits": "Хаам бо зӀе чухул, цхьа жимма а хийцамаш биняхь",
        "tog-enotifrevealaddr": "Гайта сан зlе оцу хаамаш барехь",
        "tog-shownumberswatching": "Гайта декъашхойн терахь, агӀо латийна болу шай тергаме могӀанан юкъа",
-       "tog-oldsig": "Карара куьгтаӀорна:",
+       "tog-oldsig": "Карара хьан куьг:",
        "tog-fancysig": "Шен вики-къастаман куьгтаӀдар (ша шех хьажорг йоцуш)",
        "tog-uselivepreview": "Лелае чехка хьалха хьажар",
        "tog-forceeditsummary": "Дага даийта, нагахь нисйарх лаьцна чохь язйина яцахь",
        "recentchanges-legend-heading": "<strong>Легенда:&nbsp;</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (хьажа кхин [[Special:NewPages|керла агӀонийн могӀа]])",
        "recentchanges-submit": "Гайта",
-       "rcfilters-savedqueries-defaultlabel": "Ӏалашъе луьттург",
+       "rcfilters-other-review-tools": "<strong>Талларан кхин гӀирсаш</strong>",
+       "rcfilters-activefilters": "Жигара литтарш",
+       "rcfilters-limit-title": "Гойту хийцамаш",
+       "rcfilters-limit-shownum": "Гайта тӀеххьара {{PLURAL:$1|хийцам}}",
+       "rcfilters-days-title": "ТӀеххьара денош",
+       "rcfilters-hours-title": "ТӀеххьара сахьташ",
+       "rcfilters-days-show-days": "$1 {{PLURAL:$1|де}}",
+       "rcfilters-days-show-hours": "$1 {{PLURAL:$1|сахьт}}",
+       "rcfilters-quickfilters": "Ӏалашдина литтарш",
+       "rcfilters-savedqueries-defaultlabel": "Ӏалашдина литтарш",
        "rcfilters-savedqueries-rename": "ЦӀе хийцар",
        "rcfilters-savedqueries-setdefault": "Ӏадйитаран кеп хӀоттае",
        "rcfilters-savedqueries-unsetdefault": "Ӏадйитаран кеп дӀаяккха",
        "rcfilters-savedqueries-add-new-title": "Ӏалашде литтар нисъяр",
        "rcfilters-restore-default-filters": "Литтарш Ӏадйитаран кепе меттахӀоттае",
        "rcfilters-clear-all-filters": "Ерриге литтарш цӀанъян",
+       "rcfilters-search-placeholder": "Литтаран керла хийцамаш лахар",
        "rcfilters-filterlist-title": "Литтарш",
+       "rcfilters-filterlist-feedbacklink": "Керла (бета) литтарех лаьцна хьайна хеттарг язде",
+       "rcfilters-highlightbutton-title": "Билгалде карийнарш",
+       "rcfilters-highlightmenu-help": "Билгалонан бос харжа",
        "rcfilters-filterlist-noresults": "Литтарш цакарий",
        "rcfilters-filtergroup-authorship": "Нисде авторалла",
        "rcfilters-filter-editsbyself-label": "Хьан дисдарш",
        "rcfilters-filter-user-experience-level-newcomer-label": "Керланиш",
        "rcfilters-filter-user-experience-level-learner-label": "Доьшуш берш",
        "rcfilters-filter-bots-label": "Бот",
+       "rcfilters-filter-humans-label": "Адам ду (бот яц)",
+       "rcfilters-filter-humans-description": "Декъашхоша дина нисдарш.",
        "rcfilters-filter-patrolled-label": "Патрулйина",
+       "rcfilters-filter-pageedits-label": "АгӀонан нисдарш",
+       "rcfilters-filter-pageedits-description": "Дийцарийн а, категорийн а чулацаман дина нисдарш...",
+       "rcfilters-filter-newpages-label": "АгӀонаш кхоллар",
+       "rcfilters-filter-newpages-description": "АгӀонаш кхуллуш дина нисдарш.",
+       "rcfilters-filter-logactions-label": "ДӀаяздина динарш",
+       "rcfilters-filter-logactions-description": "Куьйгалхоша динарг, дӀабазбалар, агӀонаш дӀаяхар, файлаш чуяхар...",
        "rcfilters-filtergroup-lastRevision": "Карара верси",
        "rcfilters-filter-lastrevision-label": "Карара верси",
        "rcfilters-filter-lastrevision-description": "АгӀона уггаре тӀеххьара хийцамаш.",
        "rcfilters-filter-previousrevision-label": "Хьалхара версеш",
        "rcfilters-filter-excluded": "ДӀаяьккхина",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:not</strong> $1",
+       "rcfilters-view-advanced-filters-label": "Шуьйра литтарш",
        "rcfilters-view-tags": "Билгалонаш",
+       "rcfilters-view-namespaces-tooltip": "Меттигийн цӀерашца литтаран карийнарш",
+       "rcfilters-view-tags-tooltip": "Нисдарийн билгало йолу литтаран карийна хийцамаш",
        "rcnotefrom": "Лахахь гайтина тӀера <strong>$2</strong> (хийцамаш <strong>$1</strong> кӀезиг).",
        "rclistfromreset": "Терахь харжар дӀадаккха",
        "rclistfrom": "Гайта хийцам {{CURRENTYEAR}} шеран {{CURRENTDAY}} {{CURRENTMONTHNAMEGEN}} {{CURRENTTIME}} бина болу",
        "exif-whitebalance-0": "Къайн автоматически баланс",
        "exif-whitebalance-1": "Куьйга хӀоттийна къайн баланс",
        "exif-scenecapturetype-0": "Стандартан",
+       "exif-scenecapturetype-1": "Ландшафт",
+       "exif-scenecapturetype-2": "Сурт",
        "exif-scenecapturetype-3": "Буса даьккхина сурт",
        "exif-gaincontrol-0": "Яц",
        "exif-gaincontrol-1": "ГӀеххьа доккха дар",
        "htmlform-cloner-create": "ТӀетоха кхин",
        "htmlform-cloner-delete": "ДӀаяккха",
        "htmlform-date-placeholder": "ШШШШ-ББ-ДД",
+       "htmlform-time-placeholder": "СС:ММ:СС",
        "htmlform-datetime-placeholder": "ШШШШ-ББ-ДД СС:ММ:СС",
        "htmlform-title-not-exists": "«$1» яц.",
        "htmlform-user-not-exists": "<strong>$1</strong> яц.",
        "pagelang-language": "Мотт",
        "pagelang-use-default": "Ӏадйитаран кепаца мотт",
        "pagelang-select-lang": "Харжа мотт",
+       "pagelang-reason": "Бахьана",
        "pagelang-submit": "ДӀадахьийта",
+       "pagelang-nonexistent-page": "$1 агӀо яц.",
        "right-pagelang": "АгӀона мотт хийца",
        "action-pagelang": "агӀона мотт хийца",
        "log-name-pagelang": "Мотт хийцаран тептар",
        "date-range-to": "Терхье:",
        "sessionprovider-generic": "$1 сесси",
        "randomrootpage": "Цахууш нисъелла ораман агӀо",
+       "log-action-filter-block-block": "Блоктохар",
+       "log-action-filter-block-reblock": "Блоктохар хийцар",
+       "log-action-filter-block-unblock": "БлокдӀаяхарш",
+       "log-action-filter-rights-autopromote": "Авто хийцар",
+       "log-action-filter-upload-upload": "Керла чудаккхар",
+       "log-action-filter-upload-overwrite": "Юху чуяккха",
+       "authmanager-email-label": "Электронан пошт",
        "authmanager-provider-temporarypassword": "Ханна пароль",
        "changecredentials": "Декъашхочун дӀаяздаран хийцам",
        "removecredentials": "ДӀадаха декъашхойн дӀаяздарш",
index bea0be0..3c0bab0 100644 (file)
        "deadendpages": "Páginas sin salida",
        "deadendpagestext": "Las siguientes páginas no enlazan a otras páginas de {{SITENAME}}.",
        "protectedpages": "Páginas protegidas",
-       "protectedpages-indef": "Sólo protecciones indefinidas",
+       "protectedpages-indef": "Solo protecciones indefinidas",
        "protectedpages-summary": "Esta página enumera las páginas existentes que actualmente están protegidas. Para obtener una lista de títulos que están protegidos desde su creación, véase [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
-       "protectedpages-cascade": "Sólo protecciones en cascada",
+       "protectedpages-cascade": "Solo protecciones en cascada",
        "protectedpages-noredirect": "Ocultar redirecciones",
        "protectedpagesempty": "Actualmente no hay ninguna página protegida con esos parámetros.",
        "protectedpages-timestamp": "Fecha y hora",
        "ipblocklist-otherblocks": "{{PLURAL:$1|Otro bloqueo|Otros bloqueos}}",
        "infiniteblock": "infinito",
        "expiringblock": "expira el $1 a las $2",
-       "anononlyblock": "sólo anón.",
+       "anononlyblock": "solo anón.",
        "noautoblockblock": "bloqueo automático desactivado",
        "createaccountblock": "creación de cuenta bloqueada",
        "emailblock": "correo electrónico bloqueado",
index 06fe26f..d43ab0e 100644 (file)
        "unprotectedarticle": "صفحهٔ «[[$1]]» را از محافظت بیرون آورد",
        "movedarticleprotection": "تنظیمات محافظت را از «[[$2]]» به «[[$1]]» منتقل کرد",
        "protectedarticle-comment": "«[[$1]]» را {{GENDER:$2|محافظت کرد}}",
-       "modifiedarticleprotection-comment": "سطح محاظفت «[[$1]]» را {{GENDER:$2|تغییر داد}}",
+       "modifiedarticleprotection-comment": "سطح محافظت «[[$1]]» را {{GENDER:$2|تغییر داد}}",
        "unprotectedarticle-comment": "«[[$1]]» را از محافظت {{GENDER:$2|در آورد}}",
        "protect-title": "تغییر وضعیت محافظت «$1»",
        "protect-title-notallowed": "مشاهدهٔ سطح محافظت «$1»",
index 87183fa..3dcb491 100644 (file)
        "editingsection": "Muokataan osiota sivusta $1",
        "editingcomment": "Muokataan uutta osiota sivulla $1",
        "editconflict": "Päällekkäinen muokkaus: $1",
-       "explainconflict": "Joku muu on muuttanut tätä sivua sen jälkeen, kun aloit muokata sitä.\nYlempi tekstialue sisältää tämänhetkisen tekstin.\nTekemäsi muutokset näkyvät alemmassa ikkunassa.\nSinun täytyy yhdistää muutoksesi olemassa olevaan tekstiin.\n'''Vain''' ylemmässä alueessa oleva teksti tallentuu, kun tallennat sivun.",
+       "explainconflict": "Joku muu on muuttanut tätä sivua sen jälkeen, kun aloit muokata sitä.\nYlempi tekstialue sisältää tämänhetkisen tekstin.\nTekemäsi muutokset näkyvät alemmassa ikkunassa.\nSinun täytyy yhdistää muutoksesi olemassa olevaan tekstiin.\n<strong>Vain</strong> ylemmässä alueessa oleva teksti tallentuu, kun napsautat \"$1\".",
        "yourtext": "Oma tekstisi",
        "storedversion": "Tallennettu versio",
        "nonunicodebrowser": "'''Selaimesi ei ole Unicode-yhteensopiva. Ole hyvä ja vaihda selainta, ennen kuin muokkaat sivua.'''",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (katso myös [[Special:NewPages|lista uusista sivuista]])",
        "recentchanges-legend-plusminus": "(''±123'')",
        "recentchanges-submit": "Näytä",
+       "rcfilters-tag-remove": "Poista '$1'",
        "rcfilters-legend-heading": "<strong>Luettelo lyhenteistä:</strong>",
        "rcfilters-other-review-tools": "<strong>Muut arviointityökalut</strong>",
        "rcfilters-group-results-by-page": "Ryhmitä tulokset sivujen mukaan",
        "rcfilters-grouping-title": "Ryhmitys",
        "rcfilters-activefilters": "Aktiiviset suodattimet",
+       "rcfilters-advancedfilters": "Kehittyneet suodattimet",
        "rcfilters-limit-title": "Näytettävät muutokset",
        "rcfilters-limit-shownum": "Näytä {{PLURAL:$1|viimeisin muutos|$1 viimeisintä muutosta}}",
        "rcfilters-days-title": "Viimeisimmät päivät",
        "rcfilters-hours-title": "Viimeisimmät tunnit",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|päivä|päivää}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|tunti|tuntia}}",
+       "rcfilters-highlighted-filters-list": "Korostettu: $1",
        "rcfilters-quickfilters": "Tallennetut suodattimet",
        "rcfilters-quickfilters-placeholder-title": "Ei vielä tallennettuja linkkejä",
        "rcfilters-savedqueries-defaultlabel": "Tallennetut suodattimet",
        "rcfilters-savedqueries-unsetdefault": "Poista oletuksena",
        "rcfilters-savedqueries-remove": "Poista",
        "rcfilters-savedqueries-new-name-label": "Nimi",
+       "rcfilters-savedqueries-new-name-placeholder": "Kuvaa suodattimen tarkoitusta",
        "rcfilters-savedqueries-apply-label": "Luo suodatin",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Luo oletussuodatin",
        "rcfilters-savedqueries-cancel-label": "Peru",
        "rcfilters-filterlist-noresults": "Ei löytynyt suodattimia",
        "rcfilters-noresults-conflict": "Tuloksia ei löytynyt, koska hakuehdot ovat ristiriidassa",
        "rcfilters-state-message-subset": "Tällä suodattimella ei ole vaikutusta, koska sen tulokset sisältyvät {{PLURAL:$2|seuraavaan laajempaan suodattimeen|seuraaviin laajempiin suodattimiin}} (kokeile korostusta sen erottamiseksi): $1",
-       "rcfilters-state-message-fullcoverage": "Ryhmän kaikkien suodattimien valitseminen on sama, kuin ei valitse mitään, joten tällä suodattimella ei ole vaikutusta. Ryhmään sisältyy: $1",
+       "rcfilters-state-message-fullcoverage": "Tässä ryhmässä kaikkien suodattimien valitseminen on sama, kuin ei valitse mitään, joten tällä suodattimella ei ole vaikutusta. Ryhmään sisältyy: $1",
        "rcfilters-filtergroup-authorship": "Muutoksen tekijä",
-       "rcfilters-filter-editsbyself-label": "Muutoksesi",
+       "rcfilters-filter-editsbyself-label": "Sinun tekemät muutokset",
        "rcfilters-filter-editsbyself-description": "Tekemäsi muutokset.",
        "rcfilters-filter-editsbyother-label": "Muiden muutokset",
        "rcfilters-filter-editsbyother-description": "Muiden käyttäjien tekemät muutokset.",
        "rcfilters-filter-watchlist-watched-label": "Tarkkailulistalla",
        "rcfilters-filter-watchlist-watched-description": "Muutokset tarkkailulistalla oleviin sivuihin.",
        "rcfilters-filter-watchlist-watchednew-label": "Uudet tarkkailulistan muutokset",
+       "rcfilters-filter-watchlist-watchednew-description": "Muutokset tarkkailulistalla oleviin sivuihin, joilla et ole vieraillut sen jälkeen, kun sivuille on tehty muutoksia.",
        "rcfilters-filter-watchlist-notwatched-label": "Ei tarkkailulistalla",
        "rcfilters-filtergroup-changetype": "Muutoksen tyyppi",
        "rcfilters-filter-pageedits-label": "Sivun muokkaukset",
        "rcfilters-typeofchange-conflicts-hideminor": "\"Muutoksen tyyppi\" on ristiriidassa \"Pienet muutokset\" -suodattimen kanssa. Joitain muutostyyppejä ei voida merkitä \"pieniksi\".",
        "rcfilters-filtergroup-lastRevision": "Viimeisimmät versiot",
        "rcfilters-filter-lastrevision-label": "Viimeisin versio",
-       "rcfilters-filter-lastrevision-description": "Viimeisin muutos sivulle.",
-       "rcfilters-filter-previousrevision-label": "Aikaisemman versiot",
+       "rcfilters-filter-lastrevision-description": "Vain viimeisin muutos sivulle.",
+       "rcfilters-filter-previousrevision-label": "Ei viimeisin muutos",
+       "rcfilters-view-advanced-filters-label": "Kehittyneet suodattimet",
        "rcfilters-view-namespaces-tooltip": "Suodata tuloksia nimiavaruuden mukaan",
        "rcnotefrom": "Alla ovat muutokset <strong>$3, $4</strong> lähtien. (Enintään <strong>$1</strong> näytetään.)",
+       "rclistfromreset": "Tyhjennä ajankohdan valinta",
        "rclistfrom": "Näytä uudet muutokset $3 kello $2 alkaen",
        "rcshowhideminor": "$1 pienet muutokset",
        "rcshowhideminor-show": "Näytä",
        "newimages-user": "IP-osoite tai käyttäjänimi:",
        "newimages-showbots": "Näytä bottien tekemät tallennukset",
        "newimages-hidepatrolled": "Piilota tarkastetut tiedostotallennukset",
+       "newimages-mediatype": "Median tyyppi:",
        "noimages": "Ei uusia tiedostoja.",
        "gallery-slideshow-toggle": "Vaihda pienoiskuvaa",
        "ilsubmit": "Hae",
index fc5eb34..95f4836 100644 (file)
        "emailnotauthenticated": "Votre adresse de courriel n’est pas encore confirmée.\nAucun courriel ne sera envoyé pour chacune des fonctions suivantes.",
        "noemailprefs": "Indiquez une adresse de courriel dans vos préférences pour utiliser ces fonctions.",
        "emailconfirmlink": "Confirmez votre adresse de courriel",
-       "invalidemailaddress": "Cette adresse courriel ne peut pas être acceptée car son format paraît incorrect.\nEntrez une adresse bien formatée ou laissez ce champ vide.",
+       "invalidemailaddress": "Cette adresse courriel ne peut pas être acceptée car son format paraît incorrect.\nEntrez une adresse correctement formatée ou laissez ce champ vide.",
        "cannotchangeemail": "Les adresses de courriel des comptes ne peuvent pas être modifiées sur ce wiki.",
        "emaildisabled": "Ce site ne peut pas envoyer de courriels.",
        "accountcreated": "Compte créé",
        "botpasswords-updated-title": "Mot de passe de robots mis à jour",
        "botpasswords-updated-body": "Le mot de passe pour le robot « $1 » de l'utilisateur « $2 » a été mis à jour.",
        "botpasswords-deleted-title": "Mot de passe de robots supprimé",
-       "botpasswords-deleted-body": "Le mot de passe pour le robot « $1 » de l'utilisateur « $2 » a été supprimé.",
+       "botpasswords-deleted-body": "Le mot de passe pour le robot « $1 » de l'{{GENDER:$2|utilisateur|utilisatrice}} « $2 » a été supprimé.",
        "botpasswords-newpassword": "Le nouveau mot de passe pour se connecter à <strong>$1</strong> est <strong>$2</strong>. <em>Veuillez l’enregistrer pour y faire référence ultérieurement.</em><br> (Pour les anciens robots qui nécessitent que le nom fourni à la connexion soit le même que le nom d'utilisateur éventuel, vous pouvez aussi utiliser  <strong>$3</strong> comme nom d'utilisateur et <strong>$4</strong> comme mot de passe).",
        "botpasswords-no-provider": "BotPasswordsSessionProvider n’est pas disponible.",
        "botpasswords-restriction-failed": "Les restrictions de mot de passe de robots empêchent cette connexion.",
        "botpasswords-invalid-name": "Le nom d’utilisateur spécifié ne contient pas de séparateur de mot de passe de robots (« $1 »).",
-       "botpasswords-not-exist": "L’utilisateur « $1 » n’a pas le mot de passe de robots intitulé « $2 ».",
+       "botpasswords-not-exist": "L’{{GENDER:$1|utilisateur|utilisatrice}} « $1 » n’a pas de mot de passe de robots intitulé « $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 directement à cette page.",
index 6bfcda3..add4680 100644 (file)
@@ -30,7 +30,8 @@
                        "Matma Rex",
                        "Bhatakati aatma",
                        "YmKavishwar",
-                       "Kevin Kovadia"
+                       "Kevin Kovadia",
+                       "Drashti4"
                ]
        },
        "tog-underline": "કડીઓની નીચે લીટી (અંડરલાઇન):",
        "recentchangeslinked-feed": "આની સાથે જોડાયેલા ફેરફાર",
        "recentchangeslinked-toolbox": "આની સાથે જોડાયેલા ફેરફાર",
        "recentchangeslinked-title": "\"$1\" ને લગતા ફેરફારો",
-       "recentchangeslinked-summary": "આ એવા ફેરફારોની યાદી છે જે આ ચોક્કસ પાના (કે શ્રેણીનાં સભ્ય પાનાઓ) સાથે જોડાયેલા પાનાઓમાં તાજેતરમાં કરવામાં આવ્યા હોય.\n[[Special:Watchlist|તમારી ધ્યાનસૂચિમાં]] હોય તેવા પાનાં '''ઘાટા અક્ષર'''માં વર્ણવ્યાં છે.",
+       "recentchangeslinked-summary": "આ એવા ફેરફારોની યાદી છે જે આ ચોક્કસ પાના (કે શ્રેણીનાં સભ્ય પાનાઓ) સાથે જોડાયેલા પાનાઓમાં તાજેતરમાં કરવામાં આવ્યા હોય.\n[[Special:Watchlist|તમારી ધ્યાનસૂચિમાં]] હોય તેવા પાનાં <strong>ઘાટા અક્ષર</strong>માં વર્ણવ્યાં છે.",
        "recentchangeslinked-page": "પાનાનું નામ:",
        "recentchangeslinked-to": "આને બદલે આપેલા પાનાં સાથે જોડાયેલા લેખોમાં થયેલા ફેરફારો શોધો",
        "upload": "ફાઇલ ચડાવો",
        "apisandbox-examples": "ઉદાહરણો",
        "apisandbox-results": "પરિણામો",
        "booksources": "પુસ્તક સ્રોત",
-       "booksources-search-legend": "પà«\81સà«\8dતàª\95 àª¸à«\8dરà«\8bત àª®àª¾àª\9fà«\87 àª¶à«\8bધà«\8b",
+       "booksources-search-legend": "પુસ્તક સ્રોત શોધો",
        "booksources-isbn": "આઇએસબીએન:",
        "booksources-search": "શોધ",
        "booksources-text": "નીચે દર્શાવેલ યાદી એ કડીઓ બતાવે છે જેઓ નવા અને જૂના પુસ્તકો  વેચે છે , અને તમે માંગેલ વસ્તુ સંબંધિ વધુ મહિતી પણ ધરાવી શકે છે.",
        "version-libraries-library": "લાઇબ્રેરી",
        "version-libraries-version": "આવૃત્તિ",
        "redirect": "ફાઇલ, સભ્ય, પાનું, આવૃત્તિ, અથવા લૉગ ઓળખ વડે દિશાનિર્દેશન",
+       "redirect-summary": "આ વિશિષ્ટ પૃષ્ઠ ફાઇલને (ફાઇલ નામ આપવામાં આવે છે), એક પૃષ્ઠ (પુનરાવર્તન ID અથવા પૃષ્ઠ ID આપવામાં આવે છે), વપરાશકર્તા પૃષ્ઠ (એક આંકડાકીય વપરાશકર્તા ID આપવામાં આવે છે), અથવા લોગ એન્ટ્રી (લોગ ID ને આપવામાં આવે છે) પર પુનર્નિર્દેશન કરે છે.વપરાશ:[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], or [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "જાઓ",
        "redirect-lookup": "જુઓ:",
        "redirect-value": "કિંમત:",
index 0b67bc9..eba2522 100644 (file)
        "dberr-outofdate": "Imajte na umu da su njihova kazala našeg sadržaja možda zastarjela.",
        "dberr-cachederror": "Sljedeće je dohvaćena kopija tražene stranice, te možda nije ažurirana.",
        "htmlform-invalid-input": "Postoje problemi s dijelom Vašeg unosa",
-       "htmlform-select-badoption": "Vrijednost koju ste naveli nije ispravan izbor.",
+       "htmlform-select-badoption": "Vrijednost koju ste naveli nije vrijedeća mogućnost.",
        "htmlform-int-invalid": "Vrijednost koju ste naveli nije cijeli broj.",
        "htmlform-float-invalid": "Vrijednost koju ste naveli nije broj.",
        "htmlform-int-toolow": "Vrijednost koju ste naveli je ispod minimuma od $1",
index 2ae1aa3..3ebee17 100644 (file)
        "anontalk": "Diskusija",
        "navigation": "Nawigacija",
        "and": "&#32;a",
-       "qbfind": "Namakać",
-       "qbbrowse": "Přepytować",
-       "qbedit": "Wobdźěłać",
-       "qbpageoptions": "Tuta strona",
-       "qbmyoptions": "Moje strony",
        "faq": "Husto stajene prašenja (FAQ)",
-       "faqpage": "Project:Husto stajene prašenja (FAQ)",
        "actions": "Akcije",
        "namespaces": "Mjenowe rumy",
        "variants": "Warianty",
        "edit-local": "Lokalny wopis wobdźěłać .",
        "create": "Wutworić",
        "create-local": "Lokalny wopis přidać",
-       "editthispage": "Tutu stronu wobdźěłać",
-       "create-this-page": "Stronu wutworić",
        "delete": "wušmórnyć",
-       "deletethispage": "Stronu wušmórnyć",
-       "undeletethispage": "Tutu stronu wobnowić",
        "undelete_short": "{{PLURAL:$1|jednu wersiju|$1 wersiji|$1 wersije|$1 wersijow}} wobnowić",
        "viewdeleted_short": "{{PLURAL:$1|jednu wušmórnjenu změnu|$1 wušmórnjenej změnje|$1 wušmórnjene změny|$1 wušmórnjenych změnow}} sej wobhladać",
        "protect": "škitać",
        "protect_change": "změnić",
-       "protectthispage": "Stronu škitać",
        "unprotect": "Škit wotstronić",
-       "unprotectthispage": "Tutu stronu hižo nješkitać",
        "newpage": "Nowa strona",
-       "talkpage": "Tutu stronu diskutować",
        "talkpagelinktext": "diskusija",
        "specialpage": "Specialna strona",
        "personaltools": "Wosobinske nastroje",
-       "articlepage": "Nastawk",
        "talk": "diskusija",
        "views": "Zwobraznjenja",
        "toolbox": "Nastroje",
        "tool-link-emailuser": "{{GENDER:$1|Tutomu wužiwarjej|Tutej wužiwarce}} mejlku pósłać",
-       "userpage": "Wužiwarsku stronu pokazać",
-       "projectpage": "Projektowu stronu pokazać",
        "imagepage": "Datajowu stronu sej wobhladać",
        "mediawikipage": "Zdźělenku pokazać",
        "templatepage": "Předłohu pokazać",
        "rcfilters-invalid-filter": "Njepłaćiwy filter",
        "rcfilters-filterlist-whatsthis": "Što je to?",
        "rcfilters-highlightmenu-title": "Barbu wubrać",
-       "rcfilters-filtergroup-registration": "Registrowanje wužiwarja",
        "rcfilters-filter-user-experience-level-newcomer-label": "Nowački",
        "rcfilters-filter-user-experience-level-newcomer-description": "Mjenje hač 10 změnow a štyri aktiwne dny.",
        "rcfilters-filter-user-experience-level-learner-label": "Započatkarjo",
        "booksources-search": "Pytać",
        "booksources-text": "To je lisćina wotkazow k druhim sydłam, kotrež nowe a trjebane knihi předawaja. Tam móžeš tež dalše informacije wo knihach dóstać, kotrež pytaš:",
        "booksources-invalid-isbn": "Podate ISBN-čisło njezda so płaćiwe być; přepruwuj za zmylkami, z tym zo z orginialneho žórła kopěruješ.",
+       "magiclink-tracking-isbn": "Strony, kotrež wužiwaja magiske ISBN-wotkazy",
        "specialloguserlabel": "Wukonjer:",
        "speciallogtitlelabel": "Cil (titul abo {{ns:user}}:wužiwarske mjeno za wužiwarja):",
        "log": "Protokole",
        "fileduplicatesearch-noresults": "Žana dataja z mjenom \"$1\" namakana.",
        "specialpages": "Specialne strony",
        "specialpages-note-top": "Legenda",
-       "specialpages-note": "* Normalne specialne strony.\n* <span class=\"mw-specialpagerestricted\">Specialne strony z wobmjezowanym přistupom.</span>",
        "specialpages-group-maintenance": "Hladanske lisćiny",
        "specialpages-group-other": "Druhe specialne strony",
        "specialpages-group-login": "Přizjewić/Konto załožić",
        "compare-invalid-title": "Titul, kotryž sy podał, je njepłaćiwy.",
        "compare-title-not-exists": "Titul, kotryž sy podał, njeeksistuje.",
        "compare-revision-not-exists": "Wersija, kotruž sy podał, njeeksistuje.",
+       "diff-form": "'''formular'''",
        "dberr-problems": "Wodaj! Tute sydło ma techniske ćežkosće.",
        "dberr-again": "Počakń někotre mjeńšiny a zaktualizuj stronu.",
        "dberr-info": "(Njeje móžno na datowu banku přistup měć: $1)",
index 71c62d0..e7a8c1e 100644 (file)
        "tog-watchlisthideminor": "Nascondi le modifiche minori negli osservati speciali",
        "tog-watchlisthideliu": "Nascondi le modifiche degli utenti registrati negli osservati speciali",
        "tog-watchlistreloadautomatically": "Ricarica automaticamente l'elenco degli osservati speciali ogni volta che si modifica un filtro (richiede JavaScript)",
-       "tog-watchlistunwatchlinks": "Aggiungi link diretti segui/non seguire alle voci negli Osservati speciali (richiesto JavaScript per attivare la funzionalità)",
+       "tog-watchlistunwatchlinks": "Aggiungi collegamenti diretti per seguire/non seguire gli elementi negli osservati speciali (richiede JavaScript per utilizzare questa funzionalità)",
        "tog-watchlisthideanons": "Nascondi le modifiche degli utenti anonimi negli osservati speciali",
        "tog-watchlisthidepatrolled": "Nascondi le modifiche verificate negli osservati speciali",
        "tog-watchlisthidecategorization": "Nascondi la categorizzazione delle pagine",
        "enotif_lastdiff": "Per vedere questa modifica, vedi $1",
        "enotif_anon_editor": "utente anonimo $1",
        "enotif_body": "Gentile $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nOggetto dell'intervento, inserito dall'autore: $PAGESUMMARY $PAGEMINOREDIT\n\nContatta l'autore:\nvia posta elettronica: $PAGEEDITOR_EMAIL\nsul sito: $PAGEEDITOR_WIKI\n\nNon verranno inviate altre notifiche in caso di ulteriori attività, se non visiti la pagina dopo aver effettuato l'accesso. Inoltre, è possibile modificare le impostazioni di notifica per tutte le pagine nella lista degli osservati speciali.\n\nIl sistema di notifica di {{SITENAME}}, al tuo servizio\n\n--\nPer modificare le impostazioni delle notifiche via posta elettronica, visita \n{{canonicalurl:{{#special:Preferences}}}}\n\nPer modificare la lista degli osservati speciali, visita \n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nPer rimuovere la pagina dalla lista degli osservati speciali, visita\n$UNWATCHURL\n\nPer commentare e ricevere aiuto:\n$HELPPAGE",
+       "enotif_minoredit": "Questa è una modifica minore",
        "created": "creata",
        "changed": "modificata",
        "deletepage": "Cancella pagina",
index 0efcb3e..5c9498a 100644 (file)
        "special-characters-title-endash": "en ダッシュ",
        "special-characters-title-emdash": "em ダッシュ",
        "special-characters-title-minus": "マイナス記号",
-       "mw-widgets-dateinput-no-date": "選択されたデータ無し",
+       "mw-widgets-dateinput-no-date": "日付未選択",
        "mw-widgets-mediasearch-input-placeholder": "メディアを検索",
        "mw-widgets-mediasearch-noresults": "見つかりませんでした。",
        "mw-widgets-titleinput-description-new-page": "ページは存在しません",
index fd55c88..17b1083 100644 (file)
        "emailtarget": "Lebokaké jeneng panganggo panampa",
        "emailusername": "Jeneng panganggo:",
        "emailusernamesubmit": "Kirim",
-       "email-legend": "KIrim layang-èl nyang panganggo  {{SITENAME}} liyané",
+       "email-legend": "Kirim layang-èl menyang panganggo {{SITENAME}} liyané",
        "emailfrom": "Saka:",
        "emailto": "Kanggo:",
        "emailsubject": "Prekara:",
        "protectedarticle": "ngreksa \"[[$1]]\"",
        "modifiedarticleprotection": "ngowahi tataran rereksané \"[[$1]]\"",
        "unprotectedarticle": "nyingkiraké panjagan saka \"[[$1]]\"",
-       "movedarticleprotection": "ngalih setèlan rereksan saka \"[[$2]]\" nyang \"[[$1]]\"",
+       "movedarticleprotection": "ngalih setèlan rereksan saka \"[[$2]]\" menyang \"[[$1]]\"",
        "protectedarticle-comment": "{{GENDER:$2|Ngreksa}} \"[[$1]]\"",
        "modifiedarticleprotection-comment": "{{GENDER:$2|Ngowahi tataran rereksan}} tumrap \"[[$1]]\"",
        "unprotectedarticle-comment": "{{GENDER:$2|Nyopot rereksan}} saka \"[[$1]]\"",
        "whatlinkshere": "Sing nggayut mréné",
        "whatlinkshere-title": "Kaca mawa pranala nggayut \"$1\"",
        "whatlinkshere-page": "Kaca:",
-       "linkshere": "Kaca-kaca ing ngisor iki nggayut nyang '''[[:$1]]''':",
+       "linkshere": "Kaca-kaca ing ngisor iki nggayut menyang <strong>[[:$1]]</strong>:",
        "nolinkshere": "Ora ana kaca sing nduwé pranala menyang '''[[:$1]]'''.",
        "nolinkshere-ns": " Ora ana kaca sing nduwé pranala menyang '''[[:$1]]''' ing bilik jeneng sing kapilih.",
        "isredirect": "kaca lih-lihan",
        "export-addns": "Tambah",
        "export-download": "Simpen dadi barkas",
        "export-templates": "Lebokaké cithakan",
-       "export-pagelinks": "Lebokaké kaca sing kagayut nyang jeroning:",
+       "export-pagelinks": "Lebokaké kaca sing kagayut ing sajeroning:",
        "export-manual": "Tambah kaca kanthi manual:",
        "allmessages": "Layang sistem",
        "allmessagesname": "Jeneng",
        "tooltip-ca-delete": "Busak kaca iki",
        "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-watch": "Tambahaké kaca iki menyang pawawangané panjenengan",
        "tooltip-ca-unwatch": "Busak kaca iki saka pawawanganing sampéyan",
        "tooltip-search": "Golèk ing {{SITENAME}}",
        "tooltip-search-go": "Jujug kaca asesirah persis mangkéné yèn ana",
        "tooltip-preview": "Pratuduhing 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": "Wuwuh kaca iki nyang pawawanganing sampéyan",
+       "tooltip-watch": "Wuwuh kaca iki menyang pawawangané panjenengan",
        "tooltip-watchlistedit-normal-submit": "Busak sesirah",
        "tooltip-watchlistedit-raw-submit": "Anyari daptar pangawasan",
        "tooltip-recreate": "Gawéa kaca iki manèh senadyan tau dibusak",
        "confirm-unwatch-button": "Oké",
        "confirm-unwatch-top": "Singkiraké kaca iki saka daptar pangawasan Sampéyan?",
        "confirm-rollback-button": "YA",
-       "confirm-rollback-top": "Pulihaké besutan nyang kaca iki?",
+       "confirm-rollback-top": "Pulihaké besutan menyang kaca iki?",
        "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← kaca sadurungé",
        "imgmultipagenext": "kaca sabanjuré →",
        "pagelang-reason": "Alesan",
        "pagelang-submit": "Kirim",
        "pagelang-nonexistent-page": "Kaca $1 ora ana.",
-       "pagelang-unchanged-language": "Kaca $1 wis disetèl nyang basa $2.",
+       "pagelang-unchanged-language": "Kaca $1 wis disetèl menyang basa $2.",
        "mediastatistics-table-mimetype": "Jinis MIME",
        "mediastatistics-table-extensions": "Èkstènsi sing mungkin",
        "mediastatistics-table-count": "Cacah barkas",
index 0b5e469..ed81401 100644 (file)
        "page_first": "pirm",
        "page_last": "pask",
        "histlegend": "Skirtumai tarp versijų: pažymėkite lyginamas versijas ir spustelkite ''Enter'' klavišą arba mygtuką apačioje.<br />\nŽymėjimai: (dab) = palyginimas su naujausia versija,\n(pask) = palyginimas su prieš tai buvusia versija, S = smulkus keitimas.",
-       "history-fieldset-title": "Ieškoti istorijoje",
+       "history-fieldset-title": "Ieškoti versijų",
        "history-show-deleted": "Tik ištrinti",
        "histfirst": "seniausi",
        "histlast": "paskutiniai",
        "rcfilters-highlightmenu-help": "Pasirinkite spalvą šio elemento paryškinimui",
        "rcfilters-filterlist-noresults": "Nerastas toks filtras",
        "rcfilters-noresults-conflict": "Nerasta jokių rezultatų, nes paieškos kriterijai konfliktuoja",
-       "rcfilters-filtergroup-registration": "Vartotojo registracija",
-       "rcfilters-filter-registered-label": "Registruoti",
-       "rcfilters-filter-registered-description": "Prisijungę redaktoriai.",
-       "rcfilters-filter-unregistered-label": "Neregistruoti",
-       "rcfilters-filter-unregistered-description": "Redaktoriai, kurie nėra prisijungę.",
        "rcfilters-filter-editsbyself-label": "Jūsų keitimai",
        "rcfilters-filter-editsbyself-description": "Jūsų keitimai.",
        "rcfilters-filter-editsbyother-label": "Kitų keitimai",
        "rcfilters-filter-editsbyother-description": "Visi keitimai, išskyrus jūsų.",
        "rcfilters-filtergroup-userExpLevel": "Patirties lygis (tik registruotiems vartotojams)",
+       "rcfilters-filter-user-experience-level-registered-label": "Registruoti",
+       "rcfilters-filter-user-experience-level-registered-description": "Prisijungę redaktoriai.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Neregistruoti",
+       "rcfilters-filter-user-experience-level-unregistered-description": "Redaktoriai, kurie nėra prisijungę.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Naujokai",
        "rcfilters-filter-user-experience-level-newcomer-description": "Mažiau nei 10 keitimų ir 4 dienų aktyvumo.",
        "rcfilters-filter-user-experience-level-learner-label": "Mokiniai",
        "fileduplicatesearch-noresults": "Nėra failo pavadinimu \"$1\".",
        "specialpages": "Specialieji puslapiai",
        "specialpages-note-top": "Paaiškinimai",
-       "specialpages-note": "* Įprasti specialieji puslapiai.\n* <span class=\"mw-specialpagerestricted\">Apriboto pasiekiamumo specialieji puslapiai.</span>",
        "specialpages-group-maintenance": "Sistemos palaikymo pranešimai",
        "specialpages-group-other": "Kiti specialieji puslapiai",
        "specialpages-group-login": "Prisijungti / sukurti paskyrą",
        "compare-invalid-title": "Jūsų nurodytas pavadinimas neleistinas.",
        "compare-title-not-exists": "Pavadinimas, kurį nurodėte, neegzistuoja.",
        "compare-revision-not-exists": "Keitimas, kurį nurodėte, neegzistuoja.",
+       "diff-form": "'''forma'''",
        "dberr-problems": "Atsiprašome! Svetainei iškilo techninių problemų.",
        "dberr-again": "Palaukite kelias minutes ir perkraukite puslapį.",
        "dberr-info": "(Nepavyksta pasiekti duomenų bazės: $1)",
        "logentry-delete-delete_redir": "$1 pervadindamas {{GENDER:$2|ištrynė}} buvusį nukreipimą $3",
        "logentry-delete-restore": "$1 atkūrė puslapį $3 ($4)",
        "logentry-delete-restore-nocount": "$1 atkūrė puslapį $3",
+       "restore-count-revisions": "\n{{PLURAL:$1|1 versija|versijų: $1}}",
        "restore-count-files": "{{PLURAL:$1|1 failas|$1 failai}}",
        "logentry-delete-event": "$1 {{GENDER:$2|pakeitė}} matomumą {{PLURAL:$5|žurnalo įvykio|$5 žurnalo įvykių}} $3: $4",
        "logentry-delete-revision": "$1 {{GENDER:$2|pakeitė}} matomumą {{PLURAL:$5|versijos|$5 versijų}} puslapyje $3: $4",
index c2722ac..b323961 100644 (file)
        "page_first": "पहिलो",
        "page_last": "अन्तिम",
        "histlegend": "अन्तर चयन:संशोधनहरूको तुलनाको लागि रेडियो बाकसमा क्लिक गरेर इण्टर गर्नुहोस् अथवा तल दिएको बटनमा थिच्नुहोस् <br />\nलिजेंड: (चालू): '''({{int:cur}})''' = अवतरणको बीचमा अन्तर, '''({{int:last}})''' = पहिलाका अवतरणको बीचमा अन्तर, '''{{int:minoreditletter}}''' = सानो परिवर्तन।",
-       "history-fieldset-title": "à¤\87तिहासà¤\95à¥\8b à¤µà¤¿à¤\9aरण à¤\97रà¥\8dनà¥\87",
+       "history-fieldset-title": "सà¤\82शà¥\8bधनà¤\95ा à¤²à¤¾à¤\97ि à¤\96à¥\8bà¤\9cà¥\80 à¤\97रà¥\8dनà¥\81हà¥\8bसà¥\8d",
        "history-show-deleted": "मेटिएका मात्र",
        "histfirst": "पुरानो",
        "histlast": "नयाँ",
        "rcshowhidemine-hide": "लुकाउनुहोस्",
        "rcshowhidecategorization-show": "देखाउनुहोस्",
        "rcshowhidecategorization-hide": "लुकाउनुहोस्",
-       "rclinks": "पछिल्ला $1 परिवर्तनहरू पछिल्ला $2 दिनहरूमा",
+       "rclinks": "पछिल्ला $2 दिनहरूमा भएका पछिल्ला $1 परिवर्तनहरू देखाउनुहोस्",
        "diff": "भिन्न",
        "hist": "इतिहास",
        "hide": "लुकाउनुहोस्",
        "booksources-text": "तल दिइएको सूची नयाँ तथा पूराना किताब बेच्ने लगायत तपाईंले खोज्नु भएको किताबका बारेमा थप जानकारी भएका अन्य साइटका लिंकहरू हुन् ।",
        "booksources-invalid-isbn": "यो आइएसबीएन सहि छैन; मूल स्रोतबाट नक्कल गर्दा भएको त्रुटिको जाँच गर्नुहोस।",
        "specialloguserlabel": "निष्पादक:",
-       "speciallogtitlelabel": "लक्ष्य (शीर्षक वा प्रयोगकर्ता)",
+       "speciallogtitlelabel": "लक्ष्य (शीर्षक वा {{ns:user}}:प्रयोगकर्ताका लागि प्रयोगकर्ता नाम)",
        "log": "लगहरू",
        "logeventslist-submit": "देखाउनुहोस्",
        "all-logs-page": "सबै सार्वजनिक लगहरू",
        "tooltip-feed-rss": "यो पृष्ठको लागि RSS फिड",
        "tooltip-feed-atom": "यो पृष्ठको लागि एटम फिड",
        "tooltip-t-contributions": "{{GENDER:$1|यस प्रयोगकर्ता}}का योगदानहरूको सूची हेर्नुहोस्",
-       "tooltip-t-emailuser": "यो प्रयोगकर्तालाई इमेल पठाउनुहोस्",
+       "tooltip-t-emailuser": "{{GENDER:$1|यस प्रयोगकर्ता}}लाई इमेल पठाउनुहोस्",
        "tooltip-t-info": "यस पृष्ठको बारेमा थप जानकारी",
        "tooltip-t-upload": "फाइल अपलोड गर्ने",
        "tooltip-t-specialpages": "सबै विशेष पृष्ठहरूको सूची",
        "htmlform-cloner-delete": "हटाउने",
        "htmlform-cloner-required": "कम्तिमा एउटामा आवश्यक छ ।",
        "logentry-delete-delete": "$1 द्वारा पृष्ठ $3 {{GENDER:$2|मेटाइयो}}",
-       "logentry-delete-restore": "$3 पृष्ठ $1ले {{GENDER:$2|पुनर्स्थापित}} गरेको हो",
+       "logentry-delete-restore": "$1 {{GENDER:$2|पुनर्स्थापित}} पृष्ठ $3 ($4)",
        "logentry-delete-event": "$1 ले $3 पृष्ठको लग {{PLURAL:$5|प्रविष्टि|प्रविष्टिहरू}}को दृश्यता {{GENDER:$2|परिवर्तन गर्यो}}: $4",
        "logentry-delete-revision": "$1 ले $3 पृष्ठको {{PLURAL:$5|एक अवतरण|$5 अवतरणहरू}}को दृश्यता {{GENDER:$2|परिवर्तन गर्यो}}: $4",
        "logentry-delete-event-legacy": "$1 ले $3 पृष्ठमा लग क्रियाहरूको दृश्यता {{GENDER:$2|परिवर्तन गर्यो}}",
        "special-characters-title-emdash": "इएम ड्यास",
        "special-characters-title-minus": "घटाउने चिन्ह",
        "mw-widgets-titleinput-description-new-page": "हालसम्म पृष्ठ उपलब्ध छैन्",
-       "mw-widgets-titleinput-description-redirect": "$1 मा जाने"
+       "mw-widgets-titleinput-description-redirect": "$1 मा जाने",
+       "randomrootpage": "यादृच्छिक शीर्ष पृष्ठ"
 }
index d2c0afa..5f24aee 100644 (file)
        "prefs-watchlist-days-max": "Høgst {{PLURAL:$1|éin dag|$1 dagar}}",
        "prefs-watchlist-edits": "Talet på endringar som viser i den utvida overvakingslista:",
        "prefs-watchlist-edits-max": "Høgst 1000",
-       "prefs-watchlist-token": "Emne på overvakingslista:",
+       "prefs-watchlist-token": "Nykel for overvakingslista:",
        "prefs-misc": "Andre",
        "prefs-resetpass": "Endra passord",
        "prefs-changeemail": "↓Endre e-postadresse",
index 9c862ad..90b49a3 100644 (file)
        "anontalk": "ଆଲୋଚନା",
        "navigation": "ଦିଗବାରେଣି",
        "and": "&#32;ଓ",
-       "qbfind": "ଖୋଜନ୍ତୁ",
-       "qbbrowse": "ଦେଖିବେ",
-       "qbedit": "ସମ୍ପାଦନା (Edit)",
-       "qbpageoptions": "ଏହି ପୃଷ୍ଠାଟି",
-       "qbmyoptions": "ମୋ ପୃଷ୍ଠାଗୁଡ଼ିକ",
        "faq": "ବାରମ୍ବାର ପଚରାଯାଉଥିବା ପ୍ରଶ୍ନ",
-       "faqpage": "Project:ବାରମ୍ବାର ପଚରାଯାଉଥିବା ପ୍ରଶ୍ନ",
        "actions": "କାର୍ଯ୍ୟକ୍ରମ",
        "namespaces": "ନେମସ୍ପେସ",
        "variants": "ନିଆରା",
        "edit-local": "ସ୍ଥାନୀୟ ବିବରଣ ସମ୍ପାଦନା କରନ୍ତୁ",
        "create": "ତିଆରି କରନ୍ତୁ",
        "create-local": "ନିଜର ସ୍ଥାନୀୟ ବିବରଣ ଯୋଡ଼ନ୍ତୁ",
-       "editthispage": "ଏହି ପୃଷ୍ଠାଟିକୁ ବଦଳାଇବେ",
-       "create-this-page": "ଏହି ପୃଷ୍ଠା ତିଆରି କରିବେ",
        "delete": "ଲିଭାଇବେ",
-       "deletethispage": "ଏହି ପୃଷ୍ଠାଟି ଲିଭାଇବେ",
-       "undeletethispage": "ଏହି ପୃଷ୍ଠାଟିକୁ ଲିଭାଇବେ ନାହିଁ",
        "undelete_short": "{{PLURAL:$1|ଗୋଟିଏ ବଦଳ|$1ଟି ବଦଳ}} ଯାହା ଲିଭାସରିଛି ତାହାକୁ ପଛକୁ ଫେରାଇଦେବା",
        "viewdeleted_short": "{{PLURAL:$1|ଗୋଟିଏ ଲିଭାଯାଇଥିବା ବଦଳ|$1ଟି ଲିଭାଯାଇଥିବା ବଦଳ}} ଦେଖାଇବେ",
        "protect": "କିଳିବେ",
        "protect_change": "ବଦଳାଇବା",
-       "protectthispage": "ଏହି ପୃଷ୍ଠାଟିକୁ କିଳିବେ",
        "unprotect": "ସୁରକ୍ଷା ସ୍ତରକୁ ବଦଳାଇବେ",
-       "unprotectthispage": "ଏହି ପୃଷ୍ଠା ପାଇଁ ସୁରକ୍ଷାର ପ୍ରକାର ବଦଳାଇବେ",
        "newpage": "ନୂଆ ପୃଷ୍ଠା",
-       "talkpage": "ପୃଷ୍ଠାକୁ ଆଲୋଚନା କରନ୍ତୁ",
        "talkpagelinktext": "ଆଲୋଚନା",
        "specialpage": "ବିଶେଷ ପୃଷ୍ଠା",
        "personaltools": "ନିଜର ଟୁଲ",
-       "articlepage": "ସୂଚୀ ପୃଷ୍ଠାଟି ଦେଖାଇବେ",
        "talk": "ଆଲୋଚନା",
        "views": "ଦେଖା",
        "toolbox": "ଉପକରଣ",
        "tool-link-emailuser": "{{GENDER:$1|user}}ଙ୍କୁ ଇ-ମେଲ କରନ୍ତୁ",
-       "userpage": "ବ୍ୟବହାରକାରୀଙ୍କ ପୃଷ୍ଠା ଦେଖନ୍ତୁ",
-       "projectpage": "ପ୍ରକଳ୍ପ ପୃଷ୍ଠାଟି ଦେଖାଇବା",
        "imagepage": "ଫାଇଲ ପୃଷ୍ଠାଗୁଡ଼ିକ ଦେଖନ୍ତୁ",
        "mediawikipage": "ମେସେଜ ପୃଷ୍ଠାଟି ଦେଖାଇବେ",
        "templatepage": "ଛାଞ୍ଚ ପୃଷ୍ଠାଗୁଡ଼ିକ ଦେଖନ୍ତୁ",
        "nonunicodebrowser": "'''ଚେତାବନୀ: ଆପଣଙ୍କ ବ୍ରାଉଜରରେ ଇଉନିକୋଡ଼ ସଚଳ କରାଯାଇନାହିଁ ।'''\nଏକ ୱର୍କାଆରାଉଣ୍ଡ ଏକ ଏହିପରି ଜାଗା ଯାହା ଆପଣଙ୍କୁ ନିରାପଦ ଭାବରେ ପୃଷ୍ଠା ସମ୍ପାଦନ କରିବାରେ ସାହାଯ୍ୟ କରିଥାଏ: ଅଣ-ASCII ଅକ୍ଷରସମୂହ ସମ୍ପାଦନା ଘରେ ହେକ୍ସାଡେସିମାଲ କୋଡ଼ ରୂପେ ଦେଖାଯିବ ।",
        "editingold": "'''ଚେତାବନୀ: ଆପଣ ଏହି ପୃଷ୍ଠାର ଏକ ଅଚଳ ପୁରାତନ ସଙ୍କଳନକୁ ବଦଳାଉଛନ୍ତି ।'''\nଯଦି ଆପଣ ଏହାକୁ ସାଇତିବେ, ନୂଆ ସଙ୍କଳନ ଯାଏଁ କରାଯାଇଥିବା ସବୁ ବଦଳ ନଷ୍ଟ ହୋଇଯିବ ।",
        "yourdiff": "ତଫାତ",
-       "copyrightwarning": "ଦୟାକରି ଜାଣିରଖନ୍ତୁ ଯେ {{SITENAME}}କୁ ସବୁଯାକ ଅବଦାନ $2 ଅଧିନରେ ପ୍ରକାଶ କରାଯିବ । (ଅଧିକ ଜାଣିବା ପାଇଁ $1 ଦେଖନ୍ତୁ)\nଯଦି ଆପଣ ନିଜର ଲେଖା ନିର୍ଦୟ ଭାବେ ସମ୍ପାଦିତ ହେଉ ବୋଲି ଚାହୁଁନାହାନ୍ତି ବା ବଣ୍ଟନ କରାଯାଉ ବୋଲି ଚାହୁଁ ନାହାନ୍ତି ତେବେ ତାହା ଏଠାରେ ଦିଅନ୍ତୁ ନାହିଁ ।<br />\nଆପଣ ଆମପକ୍ଷେ ମଧ୍ୟ ପ୍ରତିଜ୍ଞା କରୁଛନ୍ତି ଯେ ଏହା ଆପଣ ନିଜେ ଲେଖିଛନ୍ତି, କିମ୍ବା ଏକ ପବ୍ଲିକ ଡୋମେନରୁ ବା ମାଗଣା ଓ ଖୋଲା ଲାଇସେନ୍ସ ଥିବା ସାଇଟରୁ ନକଲ କରି ଆଣିଛନ୍ତି ।\n'''ଅନୁମତି ବିନା ସତ୍ଵାଧିକାର ଥିବା କାମ ଏଠାରେ ଦିଅନ୍ତୁ ନାହିଁ !'''",
+       "copyrightwarning": "ଦà­\9fାà¬\95ରି à¬\9cାଣିରà¬\96ନà­\8dତà­\81 à¬¯à­\87 {{SITENAME}}à¬\95à­\81 à¬¸à¬¬à­\81ଯାà¬\95 à¬\85ବଦାନ $2 à¬\85ଧିନରà­\87 à¬ªà­\8dରà¬\95ାଶ à¬\95ରାଯିବ à¥¤ (à¬\85ଧିà¬\95 à¬\9cାଣିବା à¬ªà¬¾à¬\87à¬\81 $1 à¬¦à­\87à¬\96ନà­\8dତà­\81)\nଯଦି à¬\86ପଣ à¬¨à¬¿à¬\9cର à¬²à­\87à¬\96ା à¬¨à¬¿à¬°à­\8dଦà­\9f à¬­à¬¾à¬¬à­\87 à¬¸à¬®à­\8dପାଦିତ à¬¹à­\87à¬\89 à¬¬à­\8bଲି à¬\9aାହà­\81à¬\81ନାହାନà­\8dତି à¬¬à¬¾ à¬¬à¬£à­\8dà¬\9fନ à¬\95ରାଯାà¬\89 à¬¬à­\8bଲି à¬\9aାହà­\81à¬\81 à¬¨à¬¾à¬¹à¬¾à¬¨à­\8dତି à¬¤à­\87ବà­\87 à¬¤à¬¾à¬¹à¬¾ à¬\8fଠାରà­\87 à¬¦à¬¿à¬\85ନà­\8dତà­\81 à¬¨à¬¾à¬¹à¬¿à¬\81 à¥¤<br />\nà¬\86ପଣ à¬\86ମପà¬\95à­\8dଷà­\87 à¬®à¬§à­\8dà­\9f à¬ªà­\8dରତିà¬\9cà­\8dà¬\9eା à¬\95ରà­\81à¬\9bନà­\8dତି à¬¯à­\87 à¬\8fହା à¬\86ପଣ à¬¨à¬¿à¬\9cà­\87 à¬²à­\87à¬\96ିà¬\9bନà­\8dତି, à¬\95ିମà­\8dବା à¬\8fà¬\95 à¬ªà¬¬à­\8dଲିà¬\95 à¬¡à­\8bମà­\87ନରà­\81 à¬¬à¬¾ à¬®à¬¾à¬\97ଣା à¬\93 à¬\96à­\8bଲା à¬²à¬¾à¬\87ସà­\87ନà­\8dସ à¬¥à¬¿à¬¬à¬¾ à¬¸à¬¾à¬\87à¬\9fରà­\81 à¬¨à¬\95ଲ à¬\95ରି à¬\86ଣିà¬\9bନà­\8dତି à¥¤\n'''à¬\85ନà­\81ମତି à¬¬à¬¿à¬¨à¬¾ à¬¸à­\8dଵତà­\8dତà­\8dଵାଧିà¬\95ାର à¬¥à¬¿à¬¬à¬¾ à¬\95ାମ à¬\8fଠାରà­\87 à¬¦à¬¿à¬\85ନà­\8dତà­\81 à¬¨à¬¾à¬¹à¬¿à¬\81 !'''",
        "copyrightwarning2": "ଦୟାକରି ଜାଣିରଖନ୍ତୁ ଯେ {{SITENAME}} ସବୁଯାକ ଅବଦାନ ସମ୍ପାଦିତ ହୋଇପାରିବ, ବଦଳାଯାଇପାରିବ କିମ୍ବା ବାକି ଅବଦାନକାରୀଙ୍କ ଦେଇ କଢ଼ାଯାଇପାରିବ ।\nଯଦି ଆପଣ ନିଜର ଲେଖା ନିର୍ଦୟ ଭାବେ ସମ୍ପାଦିତ ହେଉ ବୋଲି ଚାହୁଁନାହାନ୍ତି ବା ବଣ୍ଟନ କରାଯାଉ ବୋଲି ଚାହୁଁ ନାହାନ୍ତି ତେବେ ତାହା ଏଠାରେ ଦିଅନ୍ତୁ ନାହିଁ ।<br />\nଆପଣ ଆମପକ୍ଷେ ମଧ୍ୟ ପ୍ରତିଜ୍ଞା କରୁଛନ୍ତି ଯେ ଏହା ଆପଣ ନିଜେ ଲେଖିଛନ୍ତି, କିମ୍ବା ଏକ ପବ୍ଲିକ ଡୋମେନରୁ ବା ମାଗଣା ଓ ଖୋଲା ଲାଇସେନ୍ସ ଥିବା ସାଇଟରୁ ନକଲ କରି ଆଣିଛନ୍ତି । (ଦୟାକରି ସବିଶେଷ ପାଇଁ $1 ଦେଖନ୍ତୁ) ।\n'''ଅନୁମତି ବିନା ସତ୍ଵାଧିକାର ଥିବା କାମ ଏଠାରେ ଦିଅନ୍ତୁ ନାହିଁ !'''",
        "longpageerror": "'''ଭୁଲ: ଆପଣ ଦେଇଥିବା ଲେଖାଟି {{PLURAL:$1|କିଲୋବାଇଟ|$1 କିଲୋବାଇଟ}} ଲମ୍ବା, ଯାହାକି ସବୁଠାରୁ ଅଧିକ {{PLURAL:$2|କିଲୋବାଇଟ|$2 କିଲୋବାଇଟ}} ଠାରୁ ବି ଅଧିକ ।'''\nଏହା ସାଇତାଯାଇପାରିବ ନାହିଁ ।",
        "readonlywarning": "ସୂଚନା: ଏହି ଡାଟାବେସଟି ରକ୍ଷଣାବେକ୍ଷଣା ପାଇଁ କିଳାଯାଇଛି । ତେଣୁ ଆପଣ ଆପଣା ସମ୍ପାଦନା ଏବେ ସାଇତି ପାରିବେ ନାହିଁ ।'''\nଆପଣ ଲେଖାସବୁ ଏକ ଟେକ୍ସଟ ଫାଇଲରେ ନକଲ ଏବଂ ପେଷ୍ଟ କରି ଆଗକୁ ବ୍ୟବହାର କରିବା ପାଇଁ ସାଇତି ପାରିବେ ।\n\nଏହାକୁ କିଳିଥିବା ପରିଛା ଏହି କଇଫତ ଦେଇଛନ୍ତି: $1",
        "exif-compression-2": "CCITT ଗୋଠ ୩ ୧-ବିମିୟ ବଦଳାଯାଇଥିବା ହଫମାନ ରନ ଲମ୍ବ ଏନକୋଡ଼ିଙ୍ଗ",
        "exif-compression-3": "CCITT ଗୋଠ ୩ ଫାକ୍ସ ଏନକୋଡ଼ିଙ୍ଗ",
        "exif-compression-4": "CCITT ଗୋଠ ୪ ଫାକ୍ସ ଏନକୋଡ଼ିଙ୍ଗ",
-       "exif-copyrighted-true": "ସତ୍ଵାଧିକାର ଥିବା",
+       "exif-copyrighted-true": "ସà­\8dଵତà­\8dତà­\8dଵାଧିà¬\95ାର à¬¥à¬¿à¬¬à¬¾",
        "exif-copyrighted-false": "କପିରାଇଟ ତଥ୍ୟ ଦିଆଯାଇନାହିଁ",
        "exif-unknowndate": "ଅଜଣା ତାରିଖ",
        "exif-orientation-1": "ସାଧାରଣ",
        "fileduplicatesearch-noresults": "\"$1\" ନାମରେ ଗୋଟିଏ ବି ଫାଇଲ ମିଳିଲା ନାହିଁ ।",
        "specialpages": "ବିଶେଷ ପୃଷ୍ଠା",
        "specialpages-note-top": "ଲିଜେଣ୍ଡ",
-       "specialpages-note": "* ସାଧାରଣ ବିଶେଷ ପୃଷ୍ଠାମାନ ।\n* <span class=\"mw-specialpagerestricted\">କିଳାଯାଇଥିବା ବିଶେଷ ପୃଷ୍ଠାମାନ ।</span>",
        "specialpages-group-maintenance": "ରକ୍ଷଣାବେକ୍ଷଣା ବିବରଣୀ",
        "specialpages-group-other": "ବାକି ବିଶେଷ ପୃଷ୍ଠା",
        "specialpages-group-login": "ଲଗ-ଇନ (Log in)/ ନୂଆ ଖାତା ଖୋଲିବେ (Sign up)",
index 036a941..fb05de8 100644 (file)
        "rcfilters-limit-shownum": "Title for the button that opens the operation to control how many results are shown. \n\nParameters: $1 - Number of results shown",
        "rcfilters-days-title": "Title for the options to change the number of days for the results shown.",
        "rcfilters-hours-title": "Title for the options to change the number of hours for the results shown.",
-       "rcfilters-days-show-days": "Title for the button that opens the operation to control the day range for the results. \n\nParameters: $1 - Number of days shown",
-       "rcfilters-days-show-hours": "Title for the button that opens the operation to control the hour range for the results. \n\nParameters: $1 - Number of hours shown",
+       "rcfilters-days-show-days": "Title for the button that opens the operation to control the day range for the results. \n\nParameters: $1 - Number of days shown\n{{Identical|Day}}",
+       "rcfilters-days-show-hours": "Title for the button that opens the operation to control the hour range for the results. \n\nParameters: $1 - Number of hours shown\n{{Identical|Hour}}",
        "rcfilters-highlighted-filters-list": "Text for the tooltip that is displayed over highlighted results, specifying which filters the result matches in [[Special:RecentChanges]] when RCFilters are enabled. \n\nParameters: $1 - A comma separated list of matching filter names.",
        "rcfilters-quickfilters": "Label for the button that opens the saved filter settings menu in [[Special:RecentChanges]]",
        "rcfilters-quickfilters-placeholder-title": "Title for the text shown in the quick filters menu on [[Special:RecentChanges]] if the user has not saved any quick filters.",
index 2593dad..edb1ff6 100644 (file)
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|измена од ваше последње посете}}",
        "enhancedrc-history": "историја",
        "recentchanges": "Скорашње измене",
-       "recentchanges-legend": "Ð\9fоÑ\81Ñ\82авке скорашњих измена",
+       "recentchanges-legend": "Ð\9eпÑ\86иÑ\98е скорашњих измена",
        "recentchanges-summary": "Пратите скорашње измене на овој страници.",
        "recentchanges-noresult": "Нема промена у задатом времену за задате критеријуме.",
        "recentchanges-feed-description": "Пратите скорашње измене уз помоћ овог довода.",
index 8f103a9..64a7353 100644 (file)
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|izmena od vaše poslednje posete}}",
        "enhancedrc-history": "istorija",
        "recentchanges": "Skorašnje izmene",
-       "recentchanges-legend": "Postavke skorašnjih izmena",
+       "recentchanges-legend": "Opcije skorašnjih izmena",
        "recentchanges-summary": "Pratite skorašnje izmene na ovoj stranici.",
        "recentchanges-noresult": "Nema promena u zadatom vremenu za zadate kriterijume.",
        "recentchanges-feed-description": "Pratite skorašnje izmene uz pomoć ovog dovoda.",
index 7a8df59..3619406 100644 (file)
        "cannotloginnow-title": "Teu bisa asup log ayeuna",
        "cannotloginnow-text": "Kaluar log teu mungkin bisa nalika keur ngagunakeun $1.",
        "cannotcreateaccount-title": "Teu bisa nyieun akun",
+       "cannotcreateaccount-text": "Nangtukeun akun langsung teu aktif di ieu wiki.",
        "yourdomainname": "Domain anjeun",
        "password-change-forbidden": "Anjeun teu bisa ngarobah kecap sandi dina ieu wiki.",
        "externaldberror": "Aya kasalahan dina pangkalan data oténtikasi luar, atawa anjeun mémang teu diwenangkeun pikeun ngaropéa akun luar anjeun.",
        "userlogin-resetpassword-link": "Poho kecap sandi?",
        "userlogin-helplink2": "Pitulung asup log",
        "userlogin-loggedin": "Anjeun geus asup log salaku {{GENDER:$1|$1}}.\nPaké pormulir di handap pikeun asup log salaku pamaké séjén.",
+       "userlogin-reauth": "Anjeun kudu asup log deui pikeun mariksa yén anjeun téh {{GENDER:$1|$1}}",
        "userlogin-createanother": "Jieun akun séjén",
        "createacct-emailrequired": "Alamat surélék:",
        "createacct-emailoptional": "Alamat surélék (teu wajib)",
        "createacct-email-ph": "Asupkeun alamat surélék anjeun",
        "createacct-another-email-ph": "Asupkeun alamat surélék",
        "createaccountmail": "Paké kecap sandi acak sarta kirim ka alamat surélék",
+       "createaccountmail-help": "Bisa dipaké pikeun nyieun akun jang batur tanpa kudu nyaho kecap sandina.",
        "createacct-realname": "Ngaran asli (teu wajib)",
        "createacct-reason": "Alesan",
        "createacct-reason-ph": "Naha bet nyieun akun séjén",
        "nocookiesnew": "Akun pamaké geus dijieun, tapi anjeun can asup log. {{SITENAME}} maké ''cookies'' pikeun ngalog pamaké. Anjeun boga ''cookies'' nu ditumpurkeun. Mangga fungsikeun, teras asup log migunakeun sandiasma sarta kecap sandi anu anyar.",
        "nocookieslogin": "{{SITENAME}} migunakeun ''cookies'' pikeun ngasupkeun pamaké kana log. Anjeun boga ''cookies'' nu ditumpurkeun. Mangga pungsikeun sarta cobian deui.",
        "nocookiesfornew": "Akun pamaké teu bisa dijieun, kusabab sumberna teu bisa dipastikeun.\nPariksa kukisna bisi tumpur, terus cobaan muat ulang ieu kaca.",
+       "createacct-loginerror": "Akun anjeun geu dijieun, ngan anjeun teu bisa langsung asup log sacara otomatis. Pék tuluykeun kana [[Special:UserLogin|asup log manual]].",
        "noname": "Anjeun teu nuliskeun ngaran pamaké nu sah.",
        "loginsuccesstitle": "Geus asup log",
        "loginsuccess": "Anjeun ayeuna geus asup log ka {{SITENAME}} salaku \"$1\".",
        "wrongpasswordempty": "Sandina can kaeusian. Cobaan deui!",
        "passwordtooshort": "Sandina kudu diwangun ku sahanteuna {{PLURAL:$1|1 karakter|$1 karakter}}.",
        "passwordtoolong": "Kecap sandi teu bisa leuwih ti {{PLURAL:$1|1 karakter|$1 karakter}}.",
+       "passwordtoopopular": "Kecap sandi nu guyub teu bisa dipaké. Mangga pilih kecap sandi nu béda.",
        "password-name-match": "Kecap sandi anjeun kudu béda ti sandiasma.",
        "password-login-forbidden": "Sandiasma jeung sandina teu bisa dipaké.",
        "mailmypassword": "Setél ulang kecap sandi",
        "changepassword-success": "Kecap sandi Anjeun geus laksana dirobah!",
        "changepassword-throttled": "Anjeun geus loba teuing nyobaan asup log.\nTungguan $1 méméh nyobaan deui.",
        "botpasswords": "Kecap sandi bot",
+       "botpasswords-summary": "<em>Kecap sandi bot</em> ngamumkinkeun aksés kana akun pamaké ngagunakeun API tanpa maké krédensial asup log utama éta akun. Hak pamaké nu nyangkaruk nalika asup log kalawn kecap sandi bot meureun bakal diwatesan.\n\nUpama anjeun teu apal kunaon anjeun mikahayang ieu lalampahan, alusna mah ulah dilakonan. Saenyana euweuh jalma lian nu digaékeun pikeun nyieun sarta nyumerahkeun kecap sandi ieu bot ka manéhna.",
        "botpasswords-disabled": "Kecap sandi bot dipareuman.",
        "botpasswords-no-central-id": "Pikeun migunakeun kecap sandi bot, anjeun kudu asup log ka akun museur heula.",
        "botpasswords-existing": "Kecap sandi bot sayaga",
        "botpasswords-label-grants": "Aksés nu dibikeun:",
        "botpasswords-label-grants-column": "Diwidian",
        "botpasswords-bad-appid": "Ngaran bot \"$1\" teu sah.",
+       "botpasswords-insert-failed": "Gagal nambahkeun ngaran bot \"$1\". Éta geus pernah ditambahkeun sugah?",
        "botpasswords-update-failed": "Gagal nganyarkeun ngaran bot \"$1\". Baheula pernah dipupus kitu?",
        "botpasswords-created-title": "Kecap sandi bot dijieun",
        "botpasswords-created-body": "Kecap sandi pikeun ngaran bot \"$1\" ti pamaké \"$2\" geus dijieun.",
        "botpasswords-updated-body": "Kecap sandi pikeun ngaran bot \"$1\" ti pamaké \"$2\" geus dianyarkeun.",
        "botpasswords-deleted-title": "Kecap sandi bot dihapus",
        "botpasswords-deleted-body": "Kecap sandi pikeun ngaran bot \"$1\" ti pamaké \"$2\" geus dipupus.",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider teu sayaga.",
+       "botpasswords-restriction-failed": "Wates kecap sandi ngahalangan ieu asup log.",
+       "botpasswords-invalid-name": "Ngaran pamaké nu dibikeun teu ngandung pamisah kecap sandi bot (\"$1\").",
+       "botpasswords-not-exist": "Pamaké \"$1\" teu miboga kecap sandi bot nu ngaranna \"$2\".",
        "resetpass_forbidden": "Sandi henteu bisa dirobah",
        "resetpass_forbidden-reason": "Kecap sandi teu bisa diganti: $1",
        "resetpass-no-info": "Anjeun kudu asup log pkeun bisa muka ieu kaca sacara langsung.",
        "passwordreset-emailtext-user": "Pamaké $1 di {{SITENAME}} ménta nyetél ulang sandi anjeun di {{SITENAME}} ($4). {{PLURAL:$3|Akun}} di handap tumali jeung alamat surélék ieu:\n\n$2\n\n{{PLURAL:$3|Ieu sandi saheulaanan}} bakal kadaluwarsa dina témpo {{PLURAL:$5|sapoé|$5 poé}}.\nAnjeun kudu asup sarta milih sandi anyar ayeuna. Lamun henteu rumasa nyieun ieu pamundut atawa lamun geus inget sandi asli sarta moal ngarobah deui, ieu talatah teu kudu dipaliré.",
        "passwordreset-emailelement": "Sandiasma: \n$1\n\nSandi saheulaanan: \n$2",
        "passwordreset-emailsentemail": "Mun ieu alamat surélék patalian jeung akun anjeun, mangka surélék pikeun nyetél ulang kecap sandi bakal dikirim.",
+       "passwordreset-emailsentusername": "Mun ieu alamat surélék patalian jeung akun anjeun, mangka surélék pikeun nyetél ulang kecap sandi bakal dikirim.",
        "passwordreset-nocaller": "Panggero kudu dibikeun",
        "passwordreset-nosuchcaller": "Panggero can aya: $1",
+       "passwordreset-ignored": "Pamulangan kecap sandi teu kasiwer. Sugan panyadia can diatur?",
        "passwordreset-invalidemail": "Alamat surélék teu sah",
        "passwordreset-nodata": "Boh ngaran pamaké, boh alamat surélék teu dibéré",
        "changeemail": "Ganti atawa pupus alamat surélék",
        "right-unblockself": "buka peungpeuk sorangan",
        "right-protect": "Ngarobah hambalan protéksi jeung édit kaca anu dikonci",
        "right-editprotected": "Edit kaca anu dikonci salaku \"{{int:protect-level-sysop}}\"",
+       "right-editcontentmodel": "Édit modél kontén kaca",
        "right-editinterface": "Édit antarbenget pamaké",
        "right-editusercss": "Édit berkas CSS pamaké séjén",
        "right-edituserjs": "Ngédit berkas JS pamaké séjén",
+       "right-editmyusercss": "Édit berkas CSS pamaké anjeun",
+       "right-editmyuserjs": "Édit berkas JavaScript pamaké anjeun",
        "right-viewmywatchlist": "Tempo awaskeuneun anjeun",
        "right-viewmyprivateinfo": "Tempo data pribadi anjeun (alamat surélék, ngaran asli)",
        "right-editmyprivateinfo": "Robah data pribadi anjeun (alamat surélék, ngaran asli)",
        "filepage-nofile": "Taya berkas nu ngaranna kieu.",
        "uploadnewversion-linktext": "ngamuatkeun vérsi anyar koropak ieu",
        "shared-repo-from": "ti $1",
+       "shared-repo": "hiji répositori réréongan",
        "upload-disallowed-here": "Anjeun teu bisa nimpah ieu berkas.",
        "filerevert": "balikkeun $1",
        "filerevert-legend": "Balikkeun gambar",
        "randompage": "Kaca acak",
        "randompage-nopages": "Euweuh kaca dina {{PLURAL:$2|ngaranspasi}} di handap: $1.",
        "randomincategory": "Kaca acak dina kategori",
+       "randomincategory-invalidcategory": "\"$1\" lain ngaran kategori nu lumaku.",
        "randomincategory-nopages": "Euweuh kaca dina [[:Category:$1]].",
        "randomincategory-category": "Kategori:",
        "randomincategory-legend": "Kaca acak dina kategori",
        "statistics-users-active-desc": "Kontributor nu ngoprék salila {{PLURAL:$1|poé|$1 poé}} panungtung",
        "pageswithprop": "Kaca ku kaca properti",
        "pageswithprop-legend": "Kaca kalawan kaca properti",
+       "pageswithprop-text": "Ieu kaca eusina daptar kaca nu ngagunakeun properti kaca tinangtu.",
        "pageswithprop-prop": "Ngarab properti:",
        "pageswithprop-reverse": "Susunkeun dina susunan tibalik",
        "pageswithprop-sortbyvalue": "Susunkeun sumasar eusi properti",
        "block-log-flags-noautoblock": "meungpeuk otomatis dipaéhan",
        "block-log-flags-noemail": "surélek di peungpeuk",
        "block-log-flags-nousertalk": "teu bisa ngédit kaca obrolan sorangan",
+       "block-log-flags-angry-autoblock": "ronjatan sistim pameungpeukan otomatis geus dihurungkeun",
        "block-log-flags-hiddenname": "sandiasma disumputkeun",
        "range_block_disabled": "Pangabisa kuncén pikeun nyieun sarupaning peungpeuk geus ditumpurkeun.",
        "ipb_expiry_invalid": "Wanci daluwarsa teu bener.",
+       "ipb_expiry_old": "Waktu kadaluwarsana mah baheula.",
        "ipb_already_blocked": "\"$1\" geus dipeungpeuk",
        "ipb-needreblock": "$1 geus dipeungpeuk. Rék dirobah sétinganana?",
        "ipb-otherblocks-header": "{{PLURAL:$1|Peungpeuk|Peungpeuk}} lianna",
        "unblock-hideuser": "Anjeun teu bisa muka peungpeuk ieu pamaké, kusabab landihanan keur disumputkeun.",
        "ipb_cant_unblock": "Éror: ID peungpeuk $1 teu kapanggih. Sigana mah geus dibuka.",
        "ip_range_invalid": "Angka IP teu bener.",
+       "ip_range_toolarge": "Panteng blok leuwih badag tibatan /$1 teu diheugbaékeun.",
        "proxyblocker": "Pameungpeuk proxy",
        "proxyblockreason": "Alamat IP anjeun dipeungpeuk sabab mangrupa proxy muka. Mangga tepungan ''Internet service provider'' atanapi ''tech support'' anjeun, béjakeun masalah serius ieu.",
        "sorbsreason": "Alamat IP anjeun kadaptar salaku ''open proxy'' dina DNSBL anu dipaké ku {{SITENAME}}.",
        "sorbs_create_account_reason": "Alamat IP anjeun kadaptar salaku ''open proxy'' dina DNSBL. Anjeun teu bisa nyieun akun",
        "ipbblocked": "Anjeun teu bisa meungpeuk atawa muka peungpeuk séjén kontributor ku sabab anjeun sorangan keur dipeungpeuk",
+       "ipbnounblockself": "Anjeun teu diidinan pikeun muka peungpeukan sorangan.",
        "lockdb": "Konci pangkalan data",
        "unlockdb": "Buka konci pangkalan data",
        "lockdbtext": "Ngonci gudang data bakal numpurkeun kabisa sakabéh pamaké pikeun ngédit kaca, ngarobah préferénsina, ngédit awaskeuneunana, sarta hal séjén nu merlukeun parobahan na gudang data. Konfirmasikeun yén ieu nu dimaksud ku anjeun, sarta anjeun bakal muka konci gudang data nalika pangropéa anjeun geus réngsé.",
        "movenologintext": "Anjeun kudu jadi pamaké nu kadaptar tur [[Special:UserLogin|asup log]] pikeun mindahkeun kaca.",
        "movenotallowed": "Anjeung teu boga kawenangan mindahkeun kaca.",
        "movenotallowedfile": "Anjeung teu boga kawenangan mindahkeun kaca.",
+       "namespace-nosubpages": "Ngaranspasi \"$1\" teu ngidinan subkaca.",
        "newtitle": "Judul anyar:",
        "move-watch": "Awaskeuneun kaca ieu",
        "movepagebtn": "Pindahkeun kaca",
        "pageinfo-header-restrictions": "Protéksi kaca",
        "pageinfo-header-properties": "Properti kaca",
        "pageinfo-display-title": "Judul pidangan",
+       "pageinfo-default-sort": "Konci susun baku",
        "pageinfo-length": "Panjang kaca (dina bit)",
        "pageinfo-article-id": "ID kaca",
        "pageinfo-language": "Basa eusi kaca",
        "pageinfo-robot-index": "Digaékeun",
        "pageinfo-robot-noindex": "Dicaram",
        "pageinfo-watchers": "Jumlah paroris kaca",
+       "pageinfo-visiting-watchers": "Jumlah pamariksa kaca nu nyorang éditan kiwari",
        "pageinfo-few-watchers": "Kurang ti $1 {{PLURAL:$1|pangawas}}",
+       "pageinfo-few-visiting-watchers": "Rék aya atawa henteuna pamariksa kaca nu nyorang éditan kiwari",
        "pageinfo-redirects-name": "Jumlah pindahan ka ieu kaca",
        "pageinfo-firstuser": "Panyieun kaca",
+       "pageinfo-firsttime": "Tanggal panyieunan kaca",
        "pageinfo-lastuser": "Pangédit panungtung",
+       "pageinfo-lasttime": "Tanggal éditan panungtung",
        "pageinfo-edits": "Jumlah éditan",
        "pageinfo-authors": "Jumlah kontributor nu béda",
+       "pageinfo-recent-edits": "Jumlah éditan kiwari (dina $1 panungtung)",
        "pageinfo-toolboxlink": "Émbaran kaca",
        "pageinfo-redirectsto": "Alihkeun ka",
        "pageinfo-redirectsto-info": "info",
        "newimages-legend": "Saringan",
        "newimages-label": "Ngaran berkas (atawa sawaréh tina ngaranna):",
        "newimages-user": "Alamat IP atawa sandiasma",
+       "newimages-mediatype": "Tipeu média:",
        "noimages": "Taya nanaon.",
        "gallery-slideshow-toggle": "''Toggle'' miniatur",
        "ilsubmit": "Paluruh",
        "exif-xresolution": "Résolusi horizontal",
        "exif-yresolution": "Résolusi tangtung",
        "exif-stripoffsets": "Perenah data gambar",
+       "exif-rowsperstrip": "Jumlah baris per strip",
+       "exif-stripbytecounts": "Bita per strip komprési",
        "exif-jpeginterchangeformat": "Ofset ka JPEG SOI",
        "exif-jpeginterchangeformatlength": "Bit data JPEG",
+       "exif-whitepoint": "Kromatisitas titik bodas",
+       "exif-primarychromaticities": "Kromatisitas warna primér",
+       "exif-ycbcrcoefficients": "Koéfisién matriks transformasi rohang warna",
        "exif-referenceblackwhite": "Pasangan ajen rujukan hideung jeung bodas",
        "exif-datetime": "Wanci jeung titimangsa parobahan koropak",
        "exif-imagedescription": "Judul gambar",
        "exif-artist": "Pangarang",
        "exif-copyright": "Nu nyepeng hak cipta",
        "exif-exifversion": "Vérsi Exif",
+       "exif-flashpixversion": "Pangrojong vérsi Flashpix",
        "exif-colorspace": "Rohangan warna",
        "exif-componentsconfiguration": "Harti unggak komponén",
        "exif-compressedbitsperpixel": "Mode komprési gambar",
        "exif-pixelxdimension": "Lébar gambar",
        "exif-pixelydimension": "Jangkung gambar",
        "exif-usercomment": "Koméntar pamaké",
+       "exif-relatedsoundfile": "Berkas audio nu patali",
        "exif-datetimeoriginal": "Titimangsa jeung wanci dijieunna data",
        "exif-datetimedigitized": "Titimangsa jeung wanci digitisasi",
+       "exif-subsectime": "Subdetik DateTime",
+       "exif-subsectimeoriginal": "Subdetik DateTimeOriginal",
+       "exif-subsectimedigitized": "Subdetik DateTimeDigitized",
        "exif-exposuretime": "Waktu pajanan",
        "exif-exposuretime-format": "$1 detik ($2)",
        "exif-fnumber": "Nomer F",
        "exif-whitebalance": "Kasaimbangan bodas",
        "exif-digitalzoomratio": "Rasio zum digital",
        "exif-focallengthin35mmfilm": "Panjang fokus dina film 35 mm",
+       "exif-scenecapturetype": "Tipeu panéwakan",
+       "exif-gaincontrol": "Kontrol layar",
        "exif-contrast": "Kontras",
        "exif-saturation": "Saturasi",
        "exif-sharpness": "Seukeutna",
+       "exif-devicesettingdescription": "Déskripsi pangaturan alat",
+       "exif-subjectdistancerange": "Lolongkrang jarak subyék",
        "exif-imageuniqueid": "ID unik gambar",
        "exif-gpsversionid": "Vérsi tag GPS",
        "exif-gpslatituderef": "Gurat Kalér atawa Kidul",
        "exif-gpslatitude": "Gurat Lintang",
        "exif-gpslongituderef": "Gurat Wétan atawa Kulon",
        "exif-gpslongitude": "Gurat Bujur",
+       "exif-gpsaltituderef": "Référénsi jangkung",
+       "exif-gpsaltitude": "Jangkung",
        "exif-gpstimestamp": "Wanci GPS (jam atomik)",
+       "exif-gpssatellites": "Satelit pikeun pangukuran",
        "exif-gpsstatus": "Status panampa",
+       "exif-gpsmeasuremode": "Modeu pangukuran",
+       "exif-gpsdop": "Katepatan pangukuran",
        "exif-gpsspeedref": "Unit kecepatan",
+       "exif-gpsspeed": "Gancangna panarima GPS",
        "exif-gpsprocessingmethod": "Ngaran métodeu olah GPS",
        "exif-gpsareainformation": "Ngaran wewengkon GPS",
        "exif-gpsdatestamp": "Titimangsa GPS",
        "version-specialpages": "Kaca husus",
        "version-parserhooks": "Kait parser",
        "version-variables": "Variabel",
+       "version-antispam": "Panyegahan spam",
        "version-other": "Séjén",
+       "version-mediahandlers": "Pananganan média",
        "version-hooks": "Kait",
        "version-parser-extensiontags": "Tag éksténsi parser",
+       "version-parser-function-hooks": "Kait fungsi parser",
        "version-hook-name": "Ngaran kait",
        "version-hook-subscribedby": "Didaptarkeun ku",
        "version-version": "($1)",
+       "version-no-ext-name": "[tanpa ngaran]",
        "version-license": "Lisénsi MediaWiki",
        "version-ext-license": "Lisénsi",
        "version-ext-colheader-name": "Éksténsi",
        "version-software": "Sopwér nu geus diinstal",
        "version-software-product": "Produk",
        "version-software-version": "Vérsi",
+       "version-entrypoints": "URL titik éntri",
+       "version-entrypoints-header-entrypoint": "Titik éntri",
        "version-entrypoints-header-url": "URL",
        "version-libraries": "Pabukon kapasang",
        "version-libraries-library": "Pabukon",
        "fileduplicatesearch-info": "$1 × $2 piksel<br />Ukuran koropak: $3<br />Tipeu MIME: $4",
        "fileduplicatesearch-result-1": "Koropak \"$1\" teu boga duplikat idéntik.",
        "fileduplicatesearch-result-n": "Koropak \"$1\" mibanda {{PLURAL:$2|1 duplikat idéntik|$2 duplikat idéntik}}.",
+       "fileduplicatesearch-noresults": "Teu manggihan berkas nu ngaranna \"$1\".",
        "specialpages": "Kaca husus",
        "specialpages-note-top": "Kamandang",
        "specialpages-group-maintenance": "Laporan pigawéeun",
        "tags-tag": "Ngaran tag",
        "tags-source-header": "Sumber",
        "tags-active-header": "Hurung?",
+       "tags-hitcount-header": "Parobahan kalawan tag",
        "tags-actions-header": "Tarékah",
        "tags-active-yes": "Enya",
        "tags-active-no": "Teu",
+       "tags-source-extension": "Ditangtukeun ku pakakas lemes",
+       "tags-source-none": "Teu dipaké deui",
        "tags-edit": "édit",
+       "tags-delete": "pupus",
        "tags-activate": "hurungkeun",
        "tags-deactivate": "pareuman",
        "tags-hitcount": "$1 {{PLURAL:$1|parobahan|parobahan}}",
        "tags-create-tag-name": "Ngaran tag:",
        "tags-create-reason": "Alesan:",
        "tags-create-submit": "Jieun",
+       "tags-create-already-exists": "Tag \"$1\" geus aya.",
+       "tags-create-warnings-below": "Anjeun rék nuluykeun panyieunan ieu tag?",
        "tags-delete-title": "Pupus tag",
        "tags-delete-reason": "Alesan:",
        "tags-activate-title": "Hurungkeun tag",
        "tags-deactivate-reason": "Alesan:",
        "tags-deactivate-submit": "Pareuman",
        "tags-edit-title": "Édit tag",
+       "tags-edit-existing-tags": "Tag nu aya:",
        "tags-edit-existing-tags-none": "<em>Taya</em>",
        "tags-edit-new-tags": "Tag anyar:",
+       "tags-edit-add": "Tambahkeun ieu tag-tag:",
        "tags-edit-remove": "Pupus ieu tag-tag:",
+       "tags-edit-remove-all-tags": "(pupus sakum tag)",
+       "tags-edit-chosen-placeholder": "Pilih sawatara tag",
+       "tags-edit-chosen-no-results": "Euweuh tag nu luyu",
        "tags-edit-reason": "Alesan:",
+       "tags-edit-revision-submit": "Larapkeun parobahan kana {{PLURAL:$1|ieu révisi|$1 révisi}}",
        "comparepages": "Bandinkeun kaca",
        "compare-page1": "Kaca 1",
        "compare-page2": "Kaca 2",
        "compare-rev1": "Révisi 1",
        "compare-rev2": "Révisi 2",
        "compare-submit": "Bandingkeun",
+       "diff-form": "Béda",
+       "diff-form-oldid": "ID révisi heubeul (opsional)",
+       "diff-form-submit": "Témbongkeun bédana",
+       "permanentlink": "Tutumbu permanén",
+       "permanentlink-revid": "ID révisi",
+       "permanentlink-submit": "Tojo ka révisi",
        "dberr-problems": "Punten! Nuju aya gangguan téhnis.",
        "dberr-again": "Cobi antos sababaraha menit, lajeng dimuat ulang.",
        "dberr-info": "(Teu bisa ngaksés basis data: $1)",
+       "dberr-info-hidden": "(Teu bisa ngaksés basis data)",
        "dberr-usegoogle": "Kanggo samentawis, tiasa dicobi milari di Google.",
        "htmlform-select-badoption": "Niléy anu diasupkeun teu bener.",
        "htmlform-float-invalid": "Niléy anu diasupkeun lain angka.",
        "htmlform-no": "Henteu",
        "htmlform-yes": "Enya",
        "htmlform-chosen-placeholder": "Pilih opsi",
+       "htmlform-cloner-create": "Tambahkeun leuwih loba",
        "htmlform-cloner-delete": "Pupus",
+       "htmlform-cloner-required": "Saeutikna hiji niléy diperlukeun.",
        "htmlform-date-placeholder": "TTTT-BB-HH",
        "htmlform-time-placeholder": "JJ:MM:DD",
        "htmlform-datetime-placeholder": "TTTT-BB-HH JJ:MM:DD",
        "logentry-newusers-create": "Akun pamaké $1 geus {{GENDER:$2|dijieun}}",
        "logentry-newusers-autocreate": "Akun pamaké $1 {{GENDER:$2|dijieun}} otomatis",
        "logentry-upload-upload": "$1 {{GENDER:$2|ngamuat}} $3",
+       "log-name-tag": "Log tag",
        "rightsnone": "(euweuh)",
+       "feedback-adding": "Nambahkeun eupan balik kana kaca...",
        "feedback-back": "Balik deui",
        "feedback-cancel": "Bolay",
        "feedback-close": "Anggeus",
        "duration-decades": "$1 {{PLURAL:$1|dékadeu|dékadeu}}",
        "duration-centuries": "$1 {{PLURAL:$1|abad|abad}}",
        "duration-millennia": "$1 {{PLURAL:$1|milénium|milénium}}",
+       "limitreport-walltime": "Pamakéan waktu nyaan",
        "expandtemplates": "Mekarkeun citakan",
        "expand_templates_input": "Téks input:",
        "expand_templates_output": "Hasil:",
        "expand_templates_xml_output": "Output XML",
+       "expand_templates_html_output": "Kaluaran HTML atah",
        "expand_templates_ok": "Heug",
        "expand_templates_remove_comments": "Pupus koméntar",
+       "expand_templates_generate_xml": "Témbongkeun tangkal parser XML",
+       "expand_templates_generate_rawhtml": "Témbongkeun HTML atah",
        "expand_templates_preview": "Pramidang",
+       "pagelanguage": "Robah basa kaca",
        "pagelang-name": "Kaca",
        "pagelang-language": "Basa",
+       "pagelang-use-default": "Paké basa baku",
        "pagelang-select-lang": "Pilih basa",
        "pagelang-reason": "Alesan",
        "pagelang-submit": "Kirim",
        "pagelang-nonexistent-page": "Kaca $1 euweuh.",
+       "right-pagelang": "Robah basa kaca",
+       "action-pagelang": "ngarobah basa kaca",
+       "log-name-pagelang": "Log parobahan basa",
        "mediastatistics": "Statistik média",
        "mediastatistics-table-mimetype": "Tipeu MIME",
+       "mediastatistics-table-extensions": "Éksténsi ngamumkinkeun",
        "mediastatistics-table-count": "Jumlah berkas",
        "mediastatistics-table-totalbytes": "Ukuran gabungan",
        "mediastatistics-header-unknown": "Teu dipikanyaho",
        "mediastatistics-header-audio": "Audio",
        "mediastatistics-header-video": "Vidio",
        "mediastatistics-header-multimedia": "Média beunghar",
+       "mediastatistics-header-office": "Aplikasi Office",
        "mediastatistics-header-text": "Tékstual",
+       "mediastatistics-header-executable": "Program",
+       "mediastatistics-header-archive": "Format komprési",
        "mediastatistics-header-total": "Sakumna berkas",
+       "json-error-state-mismatch": "JSON teu sah atawa cacat",
+       "json-error-syntax": "Kasalahan sintaks",
        "headline-anchor-title": "Tutumbu ka bagéan ieu",
        "special-characters-group-latin": "Latin",
        "special-characters-group-latinextended": "Éksténsi Latin",
        "log-action-filter-suppress-event": "Log samunian",
        "log-action-filter-suppress-revision": "Panyamunian révisi",
        "log-action-filter-suppress-delete": "Panyamunian kaca",
+       "log-action-filter-upload-upload": "Unjalan anyar",
+       "log-action-filter-upload-overwrite": "Unjal deui",
+       "authmanager-create-disabled": "Panyieunan akun ditumpurkeun",
+       "authmanager-authplugin-setpass-failed-title": "Parobahan kecap sandi gagal",
        "authmanager-authplugin-setpass-bad-domain": "Domain teu sah.",
+       "authmanager-autocreate-noperm": "Panyieunan akun otomatis teu diidinan.",
        "authmanager-userdoesnotexist": "Akun pamaké \"$1\" teu kadaptar.",
        "authmanager-username-help": "Sandiasma pikeun oténtikasi.",
        "authmanager-password-help": "Kecap sandi pikeun oténtikasi.",
        "authmanager-email-help": "Alamat surélék",
        "authmanager-realname-label": "Ngaran asli",
        "authmanager-realname-help": "Ngaran asli pamaké",
+       "authmanager-provider-password": "Oténtikasi dumasar kecap sandi",
        "authprovider-resetpass-skip-label": "Liwatan",
+       "authprovider-resetpass-skip-help": "Liwatan pamulangan kecap sandi",
        "authform-newtoken": "Token leungit. $1",
        "authform-notoken": "Token leungit",
        "authform-wrongtoken": "Token salah",
diff --git a/maintenance/archives/patch-comment-table.sql b/maintenance/archives/patch-comment-table.sql
new file mode 100644 (file)
index 0000000..c8bf958
--- /dev/null
@@ -0,0 +1,59 @@
+--
+-- patch-comment-table.sql
+--
+-- T166732. Add a `comment` table and various columns (and temporary tables) to reference it.
+
+CREATE TABLE /*_*/comment (
+  comment_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  comment_hash INT NOT NULL,
+  comment_text BLOB NOT NULL,
+  comment_data BLOB
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
+
+CREATE TABLE /*_*/revision_comment_temp (
+  revcomment_rev int unsigned NOT NULL,
+  revcomment_comment_id bigint unsigned NOT NULL,
+  PRIMARY KEY (revcomment_rev, revcomment_comment_id)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev);
+
+CREATE TABLE /*_*/image_comment_temp (
+  imgcomment_name varchar(255) binary NOT NULL,
+  imgcomment_description_id bigint unsigned NOT NULL,
+  PRIMARY KEY (imgcomment_name, imgcomment_description_id)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/imgcomment_name ON /*_*/image_comment_temp (imgcomment_name);
+
+ALTER TABLE /*_*/revision
+  ALTER COLUMN rev_comment SET DEFAULT '';
+
+ALTER TABLE /*_*/archive
+  ALTER COLUMN ar_comment SET DEFAULT '',
+  ADD COLUMN ar_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER ar_comment;
+
+ALTER TABLE /*_*/ipblocks
+  ALTER COLUMN ipb_reason SET DEFAULT '',
+  ADD COLUMN ipb_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER ipb_reason;
+
+ALTER TABLE /*_*/image
+  ALTER COLUMN img_description SET DEFAULT '';
+
+ALTER TABLE /*_*/oldimage
+  ALTER COLUMN oi_description SET DEFAULT '',
+  ADD COLUMN oi_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER oi_description;
+
+ALTER TABLE /*_*/filearchive
+  ADD COLUMN fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER fa_deleted_reason,
+  ALTER COLUMN fa_description SET DEFAULT '',
+  ADD COLUMN fa_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER fa_description;
+
+ALTER TABLE /*_*/recentchanges
+  ADD COLUMN rc_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER rc_comment;
+
+ALTER TABLE /*_*/logging
+  ADD COLUMN log_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER log_comment;
+
+ALTER TABLE /*_*/protected_titles
+  ALTER COLUMN pt_reason SET DEFAULT '',
+  ADD COLUMN pt_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER pt_reason;
diff --git a/maintenance/migrateComments.php b/maintenance/migrateComments.php
new file mode 100644 (file)
index 0000000..4313806
--- /dev/null
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Migrate comments from pre-1.30 columns to the 'comment' table
+ *
+ * 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 Maintenance
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script that migrates comments from pre-1.30 columns to the
+ * 'comment' table
+ *
+ * @ingroup Maintenance
+ */
+class MigrateComments extends LoggedUpdateMaintenance {
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Migrates comments from pre-1.30 columns to the \'comment\' table' );
+               $this->setBatchSize( 100 );
+       }
+
+       protected function getUpdateKey() {
+               return __CLASS__;
+       }
+
+       protected function updateSkippedMessage() {
+               return 'comments already migrated.';
+       }
+
+       protected function doDBUpdates() {
+               global $wgCommentTableSchemaMigrationStage;
+
+               if ( $wgCommentTableSchemaMigrationStage < MIGRATION_WRITE_NEW ) {
+                       $this->output(
+                               "...cannot update while \$wgCommentTableSchemaMigrationStage < MIGRATION_WRITE_NEW\n"
+                       );
+                       return false;
+               }
+
+               $this->migrateToTemp(
+                       'revision', 'rev_id', 'rev_comment', 'revcomment_rev', 'revcomment_comment_id'
+               );
+               $this->migrate( 'archive', 'ar_id', 'ar_comment' );
+               $this->migrate( 'ipblocks', 'ipb_id', 'ipb_reason' );
+               $this->migrateToTemp(
+                       'image', 'img_name', 'img_description', 'imgcomment_name', 'imgcomment_description_id'
+               );
+               $this->migrate( 'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_description' );
+               $this->migrate( 'filearchive', 'fa_id', 'fa_deleted_reason' );
+               $this->migrate( 'filearchive', 'fa_id', 'fa_description' );
+               $this->migrate( 'recentchanges', 'rc_id', 'rc_comment' );
+               $this->migrate( 'logging', 'log_id', 'log_comment' );
+               $this->migrate( 'protected_titles', [ 'pt_namespace', 'pt_title' ], 'pt_reason' );
+               return true;
+       }
+
+       /**
+        * Fetch comment IDs for a set of comments
+        * @param IDatabase $dbw
+        * @param array &$comments Keys are comment names, values will be set to IDs.
+        * @return int Count of added comments
+        */
+       private function loadCommentIDs( IDatabase $dbw, array &$comments ) {
+               $count = 0;
+               $needComments = $comments;
+
+               while ( true ) {
+                       $where = [];
+                       foreach ( $needComments as $need => $dummy ) {
+                               $where[] = $dbw->makeList(
+                                       [
+                                               'comment_hash' => CommentStore::hash( $need, null ),
+                                               'comment_text' => $need,
+                                       ],
+                                       LIST_AND
+                               );
+                       }
+
+                       $res = $dbw->select(
+                               'comment',
+                               [ 'comment_id', 'comment_text' ],
+                               [
+                                       $dbw->makeList( $where, LIST_OR ),
+                                       'comment_data' => null,
+                               ],
+                               __METHOD__
+                       );
+                       foreach ( $res as $row ) {
+                               $comments[$row->comment_text] = $row->comment_id;
+                               unset( $needComments[$row->comment_text] );
+                       }
+
+                       if ( !$needComments ) {
+                               break;
+                       }
+
+                       $dbw->insert(
+                               'comment',
+                               array_map( function ( $v ) {
+                                       return [
+                                               'comment_hash' => CommentStore::hash( $v, null ),
+                                               'comment_text' => $v,
+                                       ];
+                               }, array_keys( $needComments ) ),
+                               __METHOD__
+                       );
+                       $count += $dbw->affectedRows();
+               }
+               return $count;
+       }
+
+       /**
+        * Migrate comments in a table.
+        *
+        * Assumes any row with the ID field non-zero have already been migrated.
+        * Assumes the new field name is the same as the old with '_id' appended.
+        * Blanks the old fields while migrating.
+        *
+        * @param string $table Table to migrate
+        * @param string|string[] $primaryKey Primary key of the table.
+        * @param string $oldField Old comment field name
+        */
+       protected function migrate( $table, $primaryKey, $oldField ) {
+               $newField = $oldField . '_id';
+               $primaryKey = (array)$primaryKey;
+               $pkFilter = array_flip( $primaryKey );
+               $this->output( "Beginning migration of $table.$oldField to $table.$newField\n" );
+
+               $dbw = $this->getDB( DB_MASTER );
+               $next = '1=1';
+               $countUpdated = 0;
+               $countComments = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               $table,
+                               array_merge( $primaryKey, [ $oldField ] ),
+                               [
+                                       $newField => 0,
+                                       $next,
+                               ],
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Collect the distinct comments from those rows
+                       $comments = [];
+                       foreach ( $res as $row ) {
+                               $comments[$row->$oldField] = 0;
+                       }
+                       $countComments += $this->loadCommentIDs( $dbw, $comments );
+
+                       // Update the existing rows
+                       foreach ( $res as $row ) {
+                               $dbw->update(
+                                       $table,
+                                       [
+                                               $newField => $comments[$row->$oldField],
+                                               $oldField => '',
+                                       ],
+                                       array_intersect_key( (array)$row, $pkFilter ) + [
+                                               $newField => 0
+                                       ],
+                                       __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       // Calculate the "next" condition
+                       $next = '';
+                       $prompt = [];
+                       for ( $i = count( $primaryKey ) - 1; $i >= 0; $i-- ) {
+                               $field = $primaryKey[$i];
+                               $prompt[] = $row->$field;
+                               $value = $dbw->addQuotes( $row->$field );
+                               if ( $next === '' ) {
+                                       $next = "$field > $value";
+                               } else {
+                                       $next = "$field > $value OR $field = $value AND ($next)";
+                               }
+                       }
+                       $prompt = join( ' ', array_reverse( $prompt ) );
+                       $this->output( "... $prompt\n" );
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countComments new comment(s)\n"
+               );
+       }
+
+       /**
+        * Migrate comments in a table to a temporary table.
+        *
+        * Assumes any row with the ID field non-zero have already been migrated.
+        * Assumes the new table is named "{$table}_comment_temp", and it has two
+        * columns, in order, being the primary key of the original table and the
+        * comment ID field.
+        * Blanks the old fields while migrating.
+        *
+        * @param string $oldTable Table to migrate
+        * @param string $primaryKey Primary key of the table.
+        * @param string $oldField Old comment field name
+        * @param string $newPrimaryKey Primary key of the new table.
+        * @param string $newField New comment field name
+        */
+       protected function migrateToTemp( $table, $primaryKey, $oldField, $newPrimaryKey, $newField ) {
+               $newTable = $table . '_comment_temp';
+               $this->output( "Beginning migration of $table.$oldField to $newTable.$newField\n" );
+
+               $dbw = $this->getDB( DB_MASTER );
+               $next = [];
+               $countUpdated = 0;
+               $countComments = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ $table, $newTable ],
+                               [ $primaryKey, $oldField ],
+                               [ $newPrimaryKey => null ] + $next,
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [ $newTable => [ 'LEFT JOIN', "{$primaryKey}={$newPrimaryKey}" ] ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Collect the distinct comments from those rows
+                       $comments = [];
+                       foreach ( $res as $row ) {
+                               $comments[$row->$oldField] = 0;
+                       }
+                       $countComments += $this->loadCommentIDs( $dbw, $comments );
+
+                       // Update rows
+                       $inserts = [];
+                       $updates = [];
+                       foreach ( $res as $row ) {
+                               $inserts[] = [
+                                       $newPrimaryKey => $row->$primaryKey,
+                                       $newField => $comments[$row->$oldField]
+                               ];
+                               $updates[] = $row->$primaryKey;
+                       }
+                       $this->beginTransaction( $dbw, __METHOD__ );
+                       $dbw->insert( $newTable, $inserts, __METHOD__ );
+                       $dbw->update( $table, [ $oldField => '' ], [ $primaryKey => $updates ], __METHOD__ );
+                       $countUpdated += $dbw->affectedRows();
+                       $this->commitTransaction( $dbw, __METHOD__ );
+
+                       // Calculate the "next" condition
+                       $next = [ $primaryKey . ' > ' . $dbw->addQuotes( $row->$primaryKey ) ];
+                       $this->output( "... {$row->$primaryKey}\n" );
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countComments new comment(s)\n"
+               );
+       }
+}
+
+$maintClass = "MigrateComments";
+require_once RUN_MAINTENANCE_IF_MAIN;
index e36c5b6..644fb95 100644 (file)
@@ -75,20 +75,24 @@ class Orphans extends Maintenance {
         */
        private function checkOrphans( $fix ) {
                $dbw = $this->getDB( DB_MASTER );
-               $page = $dbw->tableName( 'page' );
-               $revision = $dbw->tableName( 'revision' );
+               $commentStore = new CommentStore( 'rev_comment' );
 
                if ( $fix ) {
                        $this->lockTables( $dbw );
                }
 
+               $commentQuery = $commentStore->getJoin();
+
                $this->output( "Checking for orphan revision table entries... "
                        . "(this may take a while on a large wiki)\n" );
-               $result = $dbw->query( "
-                       SELECT *
-                       FROM $revision LEFT OUTER JOIN $page ON rev_page=page_id
-                       WHERE page_id IS NULL
-               " );
+               $result = $dbw->select(
+                       [ 'revision', 'page' ] + $commentQuery['tables'],
+                       [ 'rev_id', 'rev_page', 'rev_timestamp', 'rev_user_text' ] + $commentQuery['fields'],
+                       [ 'page_id' => null ],
+                       __METHOD__,
+                       [],
+                       [ 'page' => [ 'LEFT JOIN', [ 'rev_page=page_id' ] ] ] + $commentQuery['joins']
+               );
                $orphans = $result->numRows();
                if ( $orphans > 0 ) {
                        global $wgContLang;
@@ -100,9 +104,10 @@ class Orphans extends Maintenance {
                        ) );
 
                        foreach ( $result as $row ) {
-                               $comment = ( $row->rev_comment == '' )
-                                       ? ''
-                                       : '(' . $wgContLang->truncate( $row->rev_comment, 40 ) . ')';
+                               $comment = $commentStore->getComment( $row )->text;
+                               if ( $comment !== '' ) {
+                                       $comment = '(' . $wgContLang->truncate( $comment, 40 ) . ')';
+                               }
                                $this->output( sprintf( "%10d %10d %14s %20s %s\n",
                                        $row->rev_id,
                                        $row->rev_page,
diff --git a/maintenance/postgres/archives/patch-comment-table.sql b/maintenance/postgres/archives/patch-comment-table.sql
new file mode 100644 (file)
index 0000000..8f2b3f3
--- /dev/null
@@ -0,0 +1,27 @@
+--
+-- patch-comment-table.sql
+--
+-- T166732. Add a `comment` table, and temporary tables to reference it.
+
+CREATE SEQUENCE comment_comment_id_seq;
+CREATE TABLE comment (
+       comment_id   INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('comment_comment_id_seq'),
+       comment_hash INTEGER NOT NULL,
+       comment_text TEXT    NOT NULL,
+       comment_data TEXT
+);
+CREATE INDEX comment_hash ON comment (comment_hash);
+
+CREATE TABLE revision_comment_temp (
+       revcomment_rev        INTEGER NOT NULL,
+       revcomment_comment_id INTEGER NOT NULL,
+       PRIMARY KEY (revcomment_rev, revcomment_comment_id)
+);
+CREATE UNIQUE INDEX revcomment_rev ON revision_comment_temp (revcomment_rev);
+
+CREATE TABLE image_comment_temp (
+       imgcomment_name       TEXT NOT NULL,
+       imgcomment_comment_id INTEGER NOT NULL,
+       PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
index 03fd03a..c7ace89 100644 (file)
@@ -12,6 +12,7 @@ SET client_min_messages = 'ERROR';
 DROP SEQUENCE IF EXISTS user_user_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS page_page_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS revision_rev_id_seq CASCADE;
+DROP SEQUENCE IF EXISTS comment_comment_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS text_old_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS page_restrictions_pr_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS ipblocks_ipb_id_seq CASCADE;
@@ -132,7 +133,7 @@ CREATE TABLE revision (
   rev_id             INTEGER      NOT NULL  UNIQUE DEFAULT nextval('revision_rev_id_seq'),
   rev_page           INTEGER          NULL  REFERENCES page (page_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   rev_text_id        INTEGER          NULL, -- FK
-  rev_comment        TEXT,
+  rev_comment        TEXT         NOT NULL DEFAULT '',
   rev_user           INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
   rev_user_text      TEXT         NOT NULL,
   rev_timestamp      TIMESTAMPTZ  NOT NULL,
@@ -150,6 +151,12 @@ CREATE INDEX rev_timestamp_idx      ON revision (rev_timestamp);
 CREATE INDEX rev_user_idx           ON revision (rev_user);
 CREATE INDEX rev_user_text_idx      ON revision (rev_user_text);
 
+CREATE TABLE revision_comment_temp (
+       revcomment_rev        INTEGER NOT NULL,
+       revcomment_comment_id INTEGER NOT NULL,
+       PRIMARY KEY (revcomment_rev, revcomment_comment_id)
+);
+CREATE UNIQUE INDEX revcomment_rev ON revision_comment_temp (revcomment_rev);
 
 CREATE SEQUENCE text_old_id_seq;
 CREATE TABLE pagecontent ( -- replaces reserved word 'text'
@@ -159,6 +166,16 @@ CREATE TABLE pagecontent ( -- replaces reserved word 'text'
 );
 
 
+CREATE SEQUENCE comment_comment_id_seq;
+CREATE TABLE comment (
+  comment_id   INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('comment_comment_id_seq'),
+  comment_hash INTEGER NOT NULL,
+  comment_text TEXT    NOT NULL,
+  comment_data TEXT
+);
+CREATE INDEX comment_hash ON comment (comment_hash);
+
+
 CREATE SEQUENCE page_restrictions_pr_id_seq;
 CREATE TABLE page_restrictions (
   pr_id      INTEGER      NOT NULL  UNIQUE DEFAULT nextval('page_restrictions_pr_id_seq'),
@@ -191,7 +208,8 @@ CREATE TABLE archive (
   ar_page_id        INTEGER          NULL,
   ar_parent_id      INTEGER          NULL,
   ar_sha1           TEXT         NOT NULL DEFAULT '',
-  ar_comment        TEXT,
+  ar_comment        TEXT         NOT NULL DEFAULT '',
+  ar_comment_id     INTEGER      NOT NULL DEFAULT 0,
   ar_user           INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   ar_user_text      TEXT         NOT NULL,
   ar_timestamp      TIMESTAMPTZ  NOT NULL,
@@ -296,7 +314,8 @@ CREATE TABLE ipblocks (
   ipb_user              INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   ipb_by                INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   ipb_by_text           TEXT         NOT NULL  DEFAULT '',
-  ipb_reason            TEXT         NOT NULL,
+  ipb_reason            TEXT         NOT NULL  DEFAULT '',
+  ipb_reason_id         INTEGER      NOT NULL  DEFAULT 0,
   ipb_timestamp         TIMESTAMPTZ  NOT NULL,
   ipb_auto              SMALLINT     NOT NULL  DEFAULT 0,
   ipb_anon_only         SMALLINT     NOT NULL  DEFAULT 0,
@@ -327,7 +346,7 @@ CREATE TABLE image (
   img_media_type   TEXT,
   img_major_mime   TEXT                DEFAULT 'unknown',
   img_minor_mime   TEXT                DEFAULT 'unknown',
-  img_description  TEXT      NOT NULL,
+  img_description  TEXT      NOT NULL  DEFAULT '',
   img_user         INTEGER       NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   img_user_text    TEXT      NOT NULL,
   img_timestamp    TIMESTAMPTZ,
@@ -337,6 +356,13 @@ CREATE INDEX img_size_idx      ON image (img_size);
 CREATE INDEX img_timestamp_idx ON image (img_timestamp);
 CREATE INDEX img_sha1          ON image (img_sha1);
 
+CREATE TABLE image_comment_temp (
+       imgcomment_name       TEXT NOT NULL,
+       imgcomment_comment_id INTEGER NOT NULL,
+       PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
+
 CREATE TABLE oldimage (
   oi_name          TEXT         NOT NULL,
   oi_archive_name  TEXT         NOT NULL,
@@ -344,7 +370,8 @@ CREATE TABLE oldimage (
   oi_width         INTEGER      NOT NULL,
   oi_height        INTEGER      NOT NULL,
   oi_bits          SMALLINT         NULL,
-  oi_description   TEXT,
+  oi_description   TEXT         NOT NULL DEFAULT '',
+  oi_description_id INTEGER     NOT NULL DEFAULT 0,
   oi_user          INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   oi_user_text     TEXT         NOT NULL,
   oi_timestamp     TIMESTAMPTZ      NULL,
@@ -370,7 +397,8 @@ CREATE TABLE filearchive (
   fa_storage_key        TEXT,
   fa_deleted_user       INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   fa_deleted_timestamp  TIMESTAMPTZ  NOT NULL,
-  fa_deleted_reason     TEXT,
+  fa_deleted_reason     TEXT         NOT NULL  DEFAULT '',
+  fa_deleted_reason_id  INTEGER      NOT NULL  DEFAULT 0,
   fa_size               INTEGER      NOT NULL,
   fa_width              INTEGER      NOT NULL,
   fa_height             INTEGER      NOT NULL,
@@ -379,7 +407,8 @@ CREATE TABLE filearchive (
   fa_media_type         TEXT,
   fa_major_mime         TEXT                   DEFAULT 'unknown',
   fa_minor_mime         TEXT                   DEFAULT 'unknown',
-  fa_description        TEXT         NOT NULL,
+  fa_description        TEXT         NOT NULL DEFAULT '',
+  fa_description_id     INTEGER      NOT NULL DEFAULT 0,
   fa_user               INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   fa_user_text          TEXT         NOT NULL,
   fa_timestamp          TIMESTAMPTZ,
@@ -429,7 +458,8 @@ CREATE TABLE recentchanges (
   rc_user_text       TEXT         NOT NULL,
   rc_namespace       SMALLINT     NOT NULL,
   rc_title           TEXT         NOT NULL,
-  rc_comment         TEXT,
+  rc_comment         TEXT         NOT NULL  DEFAULT '',
+  rc_comment_id      INTEGER      NOT NULL  DEFAULT 0,
   rc_minor           SMALLINT     NOT NULL  DEFAULT 0,
   rc_bot             SMALLINT     NOT NULL  DEFAULT 0,
   rc_new             SMALLINT     NOT NULL  DEFAULT 0,
@@ -528,7 +558,8 @@ CREATE TABLE logging (
   log_user        INTEGER                REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   log_namespace   SMALLINT     NOT NULL,
   log_title       TEXT         NOT NULL,
-  log_comment     TEXT,
+  log_comment     TEXT         NOT NULL DEFAULT '',
+  log_comment_id  INTEGER      NOT NULL DEFAULT 0,
   log_params      TEXT,
   log_deleted     SMALLINT     NOT NULL DEFAULT 0,
   log_user_text   TEXT         NOT NULL DEFAULT '',
@@ -635,7 +666,8 @@ CREATE TABLE protected_titles (
   pt_namespace   SMALLINT    NOT NULL,
   pt_title       TEXT        NOT NULL,
   pt_user        INTEGER         NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  pt_reason      TEXT            NULL,
+  pt_reason      TEXT        NOT NULL DEFAULT '',
+  pt_reason_id   INTEGER     NOT NULL DEFAULT 0,
   pt_timestamp   TIMESTAMPTZ NOT NULL,
   pt_expiry      TIMESTAMPTZ     NULL,
   pt_create_perm TEXT        NOT NULL DEFAULT ''
index b93d112..a2cf3c5 100644 (file)
@@ -80,6 +80,8 @@ class RebuildRecentchanges extends Maintenance {
         */
        private function rebuildRecentChangesTablePass1() {
                $dbw = $this->getDB( DB_MASTER );
+               $revCommentStore = new CommentStore( 'rev_comment' );
+               $rcCommentStore = new CommentStore( 'rc_comment' );
 
                if ( $this->hasOption( 'from' ) && $this->hasOption( 'to' ) ) {
                        $this->cutoffFrom = wfTimestamp( TS_UNIX, $this->getOption( 'from' ) );
@@ -113,13 +115,14 @@ class RebuildRecentchanges extends Maintenance {
                }
 
                $this->output( "Loading from page and revision tables...\n" );
+
+               $commentQuery = $revCommentStore->getJoin();
                $res = $dbw->select(
-                       [ 'page', 'revision' ],
+                       [ 'revision', 'page' ] + $commentQuery['tables'],
                        [
                                'rev_timestamp',
                                'rev_user',
                                'rev_user_text',
-                               'rev_comment',
                                'rev_minor_edit',
                                'rev_id',
                                'rev_deleted',
@@ -127,19 +130,22 @@ class RebuildRecentchanges extends Maintenance {
                                'page_title',
                                'page_is_new',
                                'page_id'
-                       ],
+                       ] + $commentQuery['fields'],
                        [
                                'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
-                               'rev_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
-                               'rev_page=page_id'
+                               'rev_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
                        ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp DESC' ]
+                       [ 'ORDER BY' => 'rev_timestamp DESC' ],
+                       [
+                               'page' => [ 'JOIN', 'rev_page=page_id' ],
+                       ] + $commentQuery['joins']
                );
 
                $this->output( "Inserting from page and revision tables...\n" );
                $inserted = 0;
                foreach ( $res as $row ) {
+                       $comment = $revCommentStore->getComment( $row );
                        $dbw->insert(
                                'recentchanges',
                                [
@@ -148,7 +154,6 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_user_text' => $row->rev_user_text,
                                        'rc_namespace' => $row->page_namespace,
                                        'rc_title' => $row->page_title,
-                                       'rc_comment' => $row->rev_comment,
                                        'rc_minor' => $row->rev_minor_edit,
                                        'rc_bot' => 0,
                                        'rc_new' => $row->page_is_new,
@@ -156,10 +161,9 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_this_oldid' => $row->rev_id,
                                        'rc_last_oldid' => 0, // is this ok?
                                        'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
-                                       'rc_source' => $row->page_is_new ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT
-                                       ,
+                                       'rc_source' => $row->page_is_new ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
                                        'rc_deleted' => $row->rev_deleted
-                               ],
+                               ] + $rcCommentStore->insert( $dbw, $comment ),
                                __METHOD__
                        );
                        if ( ( ++$inserted % $this->mBatchSize ) == 0 ) {
@@ -266,25 +270,27 @@ class RebuildRecentchanges extends Maintenance {
                global $wgLogTypes, $wgLogRestrictions;
 
                $dbw = $this->getDB( DB_MASTER );
+               $logCommentStore = new CommentStore( 'log_comment' );
+               $rcCommentStore = new CommentStore( 'rc_comment' );
 
                $this->output( "Loading from user, page, and logging tables...\n" );
 
+               $commentQuery = $logCommentStore->getJoin();
                $res = $dbw->select(
-                       [ 'user', 'logging', 'page' ],
+                       [ 'user', 'logging', 'page' ] + $commentQuery['tables'],
                        [
                                'log_timestamp',
                                'log_user',
                                'user_name',
                                'log_namespace',
                                'log_title',
-                               'log_comment',
                                'page_id',
                                'log_type',
                                'log_action',
                                'log_id',
                                'log_params',
                                'log_deleted'
-                       ],
+                       ] + $commentQuery['fields'],
                        [
                                'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
                                'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
@@ -298,13 +304,14 @@ class RebuildRecentchanges extends Maintenance {
                        [
                                'page' =>
                                        [ 'LEFT JOIN', [ 'log_namespace=page_namespace', 'log_title=page_title' ] ]
-                       ]
+                       ] + $commentQuery['joins']
                );
 
                $field = $dbw->fieldInfo( 'recentchanges', 'rc_cur_id' );
 
                $inserted = 0;
                foreach ( $res as $row ) {
+                       $comment = $logCommentStore->getComment( $row );
                        $dbw->insert(
                                'recentchanges',
                                [
@@ -313,7 +320,6 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_user_text' => $row->user_name,
                                        'rc_namespace' => $row->log_namespace,
                                        'rc_title' => $row->log_title,
-                                       'rc_comment' => $row->log_comment,
                                        'rc_minor' => 0,
                                        'rc_bot' => 0,
                                        'rc_patrolled' => 1,
@@ -330,7 +336,7 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_logid' => $row->log_id,
                                        'rc_params' => $row->log_params,
                                        'rc_deleted' => $row->log_deleted
-                               ],
+                               ] + $rcCommentStore->insert( $dbw, $comment ),
                                __METHOD__
                        );
 
diff --git a/maintenance/sqlite/archives/patch-comment-table.sql b/maintenance/sqlite/archives/patch-comment-table.sql
new file mode 100644 (file)
index 0000000..f743b55
--- /dev/null
@@ -0,0 +1,332 @@
+--
+-- patch-comment-table.sql
+--
+-- T166732. Add a `comment` table and various columns (and temporary tables) to reference it.
+-- Sigh, sqlite, such trouble just to change the default value of a column.
+
+CREATE TABLE /*_*/comment (
+  comment_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  comment_hash INT NOT NULL,
+  comment_text BLOB NOT NULL,
+  comment_data BLOB
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
+
+CREATE TABLE /*_*/revision_comment_temp (
+  revcomment_rev int unsigned NOT NULL,
+  revcomment_comment_id bigint unsigned NOT NULL,
+  PRIMARY KEY (revcomment_rev, revcomment_comment_id)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev);
+
+CREATE TABLE /*_*/image_comment_temp (
+  imgcomment_name varchar(255) binary NOT NULL,
+  imgcomment_description_id bigint unsigned NOT NULL,
+  PRIMARY KEY (imgcomment_name, imgcomment_description_id)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/imgcomment_name ON /*_*/image_comment_temp (imgcomment_name);
+
+ALTER TABLE /*_*/recentchanges
+  ADD COLUMN rc_comment_id bigint unsigned NOT NULL DEFAULT 0;
+
+ALTER TABLE /*_*/logging
+  ADD COLUMN log_comment_id bigint unsigned NOT NULL DEFAULT 0;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/revision_tmp;
+CREATE TABLE /*_*/revision_tmp (
+  rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  rev_page int unsigned NOT NULL,
+  rev_text_id int unsigned NOT NULL,
+  rev_comment varbinary(767) NOT NULL default '',
+  rev_user int unsigned NOT NULL default 0,
+  rev_user_text varchar(255) binary NOT NULL default '',
+  rev_timestamp binary(14) NOT NULL default '',
+  rev_minor_edit tinyint unsigned NOT NULL default 0,
+  rev_deleted tinyint unsigned NOT NULL default 0,
+  rev_len int unsigned,
+  rev_parent_id int unsigned default NULL,
+  rev_sha1 varbinary(32) NOT NULL default '',
+  rev_content_model varbinary(32) DEFAULT NULL,
+  rev_content_format varbinary(64) DEFAULT NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
+
+INSERT OR IGNORE INTO /*_*/revision_tmp (
+       rev_id, rev_page, rev_text_id, rev_comment, rev_user, rev_user_text,
+       rev_timestamp, rev_minor_edit, rev_deleted, rev_len, rev_parent_id,
+       rev_sha1, rev_content_model, rev_content_format)
+ SELECT
+       rev_id, rev_page, rev_text_id, rev_comment, rev_user, rev_user_text,
+       rev_timestamp, rev_minor_edit, rev_deleted, rev_len, rev_parent_id,
+       rev_sha1, rev_content_model, rev_content_format
+  FROM /*_*/revision;
+
+DROP TABLE /*_*/revision;
+ALTER TABLE /*_*/revision_tmp RENAME TO /*_*/revision;
+CREATE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id);
+CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp);
+CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp);
+CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp);
+CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp);
+CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/archive_tmp;
+CREATE TABLE /*_*/archive_tmp (
+  ar_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ar_namespace int NOT NULL default 0,
+  ar_title varchar(255) binary NOT NULL default '',
+  ar_text mediumblob NOT NULL,
+  ar_comment varbinary(767) NOT NULL default '',
+  ar_comment_id bigint unsigned NOT NULL DEFAULT 0,
+  ar_user int unsigned NOT NULL default 0,
+  ar_user_text varchar(255) binary NOT NULL,
+  ar_timestamp binary(14) NOT NULL default '',
+  ar_minor_edit tinyint NOT NULL default 0,
+  ar_flags tinyblob NOT NULL,
+  ar_rev_id int unsigned,
+  ar_text_id int unsigned,
+  ar_deleted tinyint unsigned NOT NULL default 0,
+  ar_len int unsigned,
+  ar_page_id int unsigned,
+  ar_parent_id int unsigned default NULL,
+  ar_sha1 varbinary(32) NOT NULL default '',
+  ar_content_model varbinary(32) DEFAULT NULL,
+  ar_content_format varbinary(64) DEFAULT NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/archive_tmp (
+       ar_id, ar_namespace, ar_title, ar_text, ar_comment, ar_user, ar_user_text,
+       ar_timestamp, ar_minor_edit, ar_flags, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model,
+       ar_content_format)
+  SELECT
+       ar_id, ar_namespace, ar_title, ar_text, ar_comment, ar_user, ar_user_text,
+       ar_timestamp, ar_minor_edit, ar_flags, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model,
+       ar_content_format
+  FROM /*_*/archive;
+
+DROP TABLE /*_*/archive;
+ALTER TABLE /*_*/archive_tmp RENAME TO /*_*/archive;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS ipblocks_tmp;
+CREATE TABLE /*_*/ipblocks_tmp (
+  ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ipb_address tinyblob NOT NULL,
+  ipb_user int unsigned NOT NULL default 0,
+  ipb_by int unsigned NOT NULL default 0,
+  ipb_by_text varchar(255) binary NOT NULL default '',
+  ipb_reason varbinary(767) NOT NULL default '',
+  ipb_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  ipb_timestamp binary(14) NOT NULL default '',
+  ipb_auto bool NOT NULL default 0,
+  ipb_anon_only bool NOT NULL default 0,
+  ipb_create_account bool NOT NULL default 1,
+  ipb_enable_autoblock bool NOT NULL default '1',
+  ipb_expiry varbinary(14) NOT NULL default '',
+  ipb_range_start tinyblob NOT NULL,
+  ipb_range_end tinyblob NOT NULL,
+  ipb_deleted bool NOT NULL default 0,
+  ipb_block_email bool NOT NULL default 0,
+  ipb_allow_usertalk bool NOT NULL default 0,
+  ipb_parent_block_id int default NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/ipblocks_tmp (
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_reason,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id)
+  SELECT
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_reason,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id
+  FROM /*_*/ipblocks;
+
+DROP TABLE /*_*/ipblocks;
+ALTER TABLE /*_*/ipblocks_tmp RENAME TO /*_*/ipblocks;
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/image_tmp;
+CREATE TABLE /*_*/image_tmp (
+  img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+  img_size int unsigned NOT NULL default 0,
+  img_width int NOT NULL default 0,
+  img_height int NOT NULL default 0,
+  img_metadata mediumblob NOT NULL,
+  img_bits int NOT NULL default 0,
+  img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  img_minor_mime varbinary(100) NOT NULL default "unknown",
+  img_description varbinary(767) NOT NULL default '',
+  img_user int unsigned NOT NULL default 0,
+  img_user_text varchar(255) binary NOT NULL,
+  img_timestamp varbinary(14) NOT NULL default '',
+  img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/image_tmp (
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+       img_user_text, img_timestamp, img_sha1)
+  SELECT
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+       img_user_text, img_timestamp, img_sha1
+  FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/oldimage_tmp;
+CREATE TABLE /*_*/oldimage_tmp (
+  oi_name varchar(255) binary NOT NULL default '',
+  oi_archive_name varchar(255) binary NOT NULL default '',
+  oi_size int unsigned NOT NULL default 0,
+  oi_width int NOT NULL default 0,
+  oi_height int NOT NULL default 0,
+  oi_bits int NOT NULL default 0,
+  oi_description varbinary(767) NOT NULL default '',
+  oi_description_id bigint unsigned NOT NULL DEFAULT 0,
+  oi_user int unsigned NOT NULL default 0,
+  oi_user_text varchar(255) binary NOT NULL,
+  oi_timestamp binary(14) NOT NULL default '',
+  oi_metadata mediumblob NOT NULL,
+  oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  oi_minor_mime varbinary(100) NOT NULL default "unknown",
+  oi_deleted tinyint unsigned NOT NULL default 0,
+  oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/oldimage_tmp (
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1)
+  SELECT
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+  FROM /*_*/oldimage;
+
+DROP TABLE /*_*/oldimage;
+ALTER TABLE /*_*/oldimage_tmp RENAME TO /*_*/oldimage;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/filearchive_tmp;
+CREATE TABLE /*_*/filearchive_tmp (
+  fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  fa_name varchar(255) binary NOT NULL default '',
+  fa_archive_name varchar(255) binary default '',
+  fa_storage_group varbinary(16),
+  fa_storage_key varbinary(64) default '',
+  fa_deleted_user int,
+  fa_deleted_timestamp binary(14) default '',
+  fa_deleted_reason varbinary(767) default '',
+  fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  fa_size int unsigned default 0,
+  fa_width int default 0,
+  fa_height int default 0,
+  fa_metadata mediumblob,
+  fa_bits int default 0,
+  fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
+  fa_minor_mime varbinary(100) default "unknown",
+  fa_description varbinary(767) default '',
+  fa_description_id bigint unsigned NOT NULL DEFAULT 0,
+  fa_user int unsigned default 0,
+  fa_user_text varchar(255) binary,
+  fa_timestamp binary(14) default '',
+  fa_deleted tinyint unsigned NOT NULL default 0,
+  fa_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/filearchive_tmp (
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason, fa_size,
+       fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+       fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp,
+       fa_deleted, fa_sha1)
+  SELECT
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason, fa_size,
+       fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+       fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp,
+       fa_deleted, fa_sha1
+  FROM /*_*/filearchive;
+
+DROP TABLE /*_*/filearchive;
+ALTER TABLE /*_*/filearchive_tmp RENAME TO /*_*/filearchive;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/protected_titles_tmp;
+CREATE TABLE /*_*/protected_titles_tmp (
+  pt_namespace int NOT NULL,
+  pt_title varchar(255) binary NOT NULL,
+  pt_user int unsigned NOT NULL,
+  pt_reason varbinary(767) default '',
+  pt_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  pt_timestamp binary(14) NOT NULL,
+  pt_expiry varbinary(14) NOT NULL default '',
+  pt_create_perm varbinary(60) NOT NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/protected_titles_tmp (
+       pt_namespace, pt_title, pt_user, pt_reason, pt_timestamp, pt_expiry, pt_create_perm)
+  SELECT
+       pt_namespace, pt_title, pt_user, pt_reason, pt_timestamp, pt_expiry, pt_create_perm
+  FROM /*_*/protected_titles;
+
+DROP TABLE /*_*/protected_titles;
+ALTER TABLE /*_*/protected_titles_tmp RENAME TO /*_*/protected_titles;
+CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title);
+CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp);
+
+COMMIT;
index 9a18796..d6ef40c 100644 (file)
@@ -345,10 +345,9 @@ CREATE TABLE /*_*/revision (
   -- or a rollback to a previous version.
   rev_text_id int unsigned NOT NULL,
 
-  -- Text comment summarizing the change.
-  -- This text is shown in the history and other changes lists,
-  -- rendered in a subset of wiki markup by Linker::formatComment()
-  rev_comment varbinary(767) NOT NULL,
+  -- Text comment summarizing the change. Deprecated in favor of
+  -- revision_comment_temp.revcomment_comment_id.
+  rev_comment varbinary(767) NOT NULL default '',
 
   -- Key to user.user_id of the user who made this edit.
   -- Stores 0 for anonymous edits and for some mass imports.
@@ -409,6 +408,23 @@ CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timesta
 -- and is a logged-in user.
 CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp);
 
+--
+-- Temporary table to avoid blocking on an alter of revision.
+--
+-- On large wikis like the English Wikipedia, altering the revision table is a
+-- months-long process. This table is being created to avoid such an alter, and
+-- will be merged back into revision in the future.
+--
+CREATE TABLE /*_*/revision_comment_temp (
+  -- Key to rev_id
+  revcomment_rev int unsigned NOT NULL,
+  -- Key to comment_id
+  revcomment_comment_id bigint unsigned NOT NULL,
+  PRIMARY KEY (revcomment_rev, revcomment_comment_id)
+) /*$wgDBTableOptions*/;
+-- Ensure uniqueness
+CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev);
+
 --
 -- Every time an edit by a logged out user is saved,
 -- a row is created in ip_changes. This stores
@@ -474,6 +490,40 @@ CREATE TABLE /*_*/text (
 -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit
 
 
+--
+-- Edits, blocks, and other actions typically have a textual comment describing
+-- the action. They are stored here to reduce the size of the main tables, and
+-- to allow for deduplication.
+--
+-- Deduplication is currently best-effort to avoid locking on inserts that
+-- would be required for strict deduplication. There MAY be multiple rows with
+-- the same comment_text and comment_data.
+--
+CREATE TABLE /*_*/comment (
+  -- Unique ID to identify each comment
+  comment_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+  -- Hash of comment_text and comment_data, for deduplication
+  comment_hash INT NOT NULL,
+
+  -- Text comment summarizing the change.
+  -- This text is shown in the history and other changes lists,
+  -- rendered in a subset of wiki markup by Linker::formatComment()
+  -- Size limits are enforced at the application level, and should
+  -- take care to crop UTF-8 strings appropriately.
+  comment_text BLOB NOT NULL,
+
+  -- JSON data, intended for localizing auto-generated comments.
+  -- This holds structured data that is intended to be used to provide
+  -- localized versions of automatically-generated comments. When not empty,
+  -- comment_text should be the generated comment localized using the wiki's
+  -- content language.
+  comment_data BLOB
+) /*$wgDBTableOptions*/;
+-- Index used for deduplication.
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
+
+
 --
 -- Holding area for deleted articles, which may be viewed
 -- or restored by admins through the Special:Undelete interface.
@@ -495,7 +545,8 @@ CREATE TABLE /*_*/archive (
   ar_text mediumblob NOT NULL,
 
   -- Basic revision stuff...
-  ar_comment varbinary(767) NOT NULL,
+  ar_comment varbinary(767) NOT NULL default '', -- Deprecated in favor of ar_comment_id
+  ar_comment_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ar_comment should be used)
   ar_user int unsigned NOT NULL default 0,
   ar_user_text varchar(255) binary NOT NULL,
   ar_timestamp binary(14) NOT NULL default '',
@@ -861,8 +912,12 @@ CREATE TABLE /*_*/ipblocks (
   -- User name of blocker
   ipb_by_text varchar(255) binary NOT NULL default '',
 
-  -- Text comment made by blocker.
-  ipb_reason varbinary(767) NOT NULL,
+  -- Text comment made by blocker. Deprecated in favor of ipb_reason_id
+  ipb_reason varbinary(767) NOT NULL default '',
+
+  -- Key to comment_id. Text comment made by blocker.
+  -- ("DEFAULT 0" is temporary, signaling that ipb_reason should be used)
+  ipb_reason_id bigint unsigned NOT NULL DEFAULT 0,
 
   -- Creation (or refresh) date in standard YMDHMS form.
   -- IP blocks expire automatically.
@@ -969,7 +1024,8 @@ CREATE TABLE /*_*/image (
 
   -- Description field as entered by the uploader.
   -- This is displayed in image upload history and logs.
-  img_description varbinary(767) NOT NULL,
+  -- Deprecated in favor of image_comment_temp.imgcomment_description_id.
+  img_description varbinary(767) NOT NULL default '',
 
   -- user_id and user_name of uploader.
   img_user int unsigned NOT NULL default 0,
@@ -994,6 +1050,23 @@ CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
 -- Used to get media of one type
 CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
 
+--
+-- Temporary table to avoid blocking on an alter of image.
+--
+-- On large wikis like Wikimedia Commons, altering the image table is a
+-- months-long process. This table is being created to avoid such an alter, and
+-- will be merged back into image in the future.
+--
+CREATE TABLE /*_*/image_comment_temp (
+  -- Key to img_name (ugh)
+  imgcomment_name varchar(255) binary NOT NULL,
+  -- Key to comment_id
+  imgcomment_description_id bigint unsigned NOT NULL,
+  PRIMARY KEY (imgcomment_name, imgcomment_description_id)
+) /*$wgDBTableOptions*/;
+-- Ensure uniqueness
+CREATE UNIQUE INDEX /*i*/imgcomment_name ON /*_*/image_comment_temp (imgcomment_name);
+
 
 --
 -- Previous revisions of uploaded files.
@@ -1013,7 +1086,8 @@ CREATE TABLE /*_*/oldimage (
   oi_width int NOT NULL default 0,
   oi_height int NOT NULL default 0,
   oi_bits int NOT NULL default 0,
-  oi_description varbinary(767) NOT NULL,
+  oi_description varbinary(767) NOT NULL default '', -- Deprecated.
+  oi_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that oi_description should be used)
   oi_user int unsigned NOT NULL default 0,
   oi_user_text varchar(255) binary NOT NULL,
   oi_timestamp binary(14) NOT NULL default '',
@@ -1061,7 +1135,8 @@ CREATE TABLE /*_*/filearchive (
   -- Deletion information, if this file is deleted.
   fa_deleted_user int,
   fa_deleted_timestamp binary(14) default '',
-  fa_deleted_reason varbinary(767) default '',
+  fa_deleted_reason varbinary(767) default '', -- Deprecated
+  fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_deleted_reason should be used)
 
   -- Duped fields from image
   fa_size int unsigned default 0,
@@ -1072,7 +1147,8 @@ CREATE TABLE /*_*/filearchive (
   fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
   fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
   fa_minor_mime varbinary(100) default "unknown",
-  fa_description varbinary(767),
+  fa_description varbinary(767) default '', -- Deprecated
+  fa_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_description should be used)
   fa_user int unsigned default 0,
   fa_user_text varchar(255) binary,
   fa_timestamp binary(14) default '',
@@ -1170,7 +1246,8 @@ CREATE TABLE /*_*/recentchanges (
   rc_title varchar(255) binary NOT NULL default '',
 
   -- as in revision...
-  rc_comment varbinary(767) NOT NULL default '',
+  rc_comment varbinary(767) NOT NULL default '', -- Deprecated.
+  rc_comment_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that rc_comment should be used)
   rc_minor tinyint unsigned NOT NULL default 0,
 
   -- Edits by user accounts with the 'bot' rights key are
@@ -1399,8 +1476,13 @@ CREATE TABLE /*_*/logging (
   log_page int unsigned NULL,
 
   -- Freeform text. Interpreted as edit history comments.
+  -- Deprecated in favor of log_comment_id.
   log_comment varbinary(767) NOT NULL default '',
 
+  -- Key to comment_id. Comment summarizing the change.
+  -- ("DEFAULT 0" is temporary, signaling that log_comment should be used)
+  log_comment_id bigint unsigned NOT NULL DEFAULT 0,
+
   -- miscellaneous parameters:
   -- LF separated list (old system) or serialized PHP array (new system)
   log_params blob NOT NULL,
@@ -1574,7 +1656,8 @@ CREATE TABLE /*_*/protected_titles (
   pt_namespace int NOT NULL,
   pt_title varchar(255) binary NOT NULL,
   pt_user int unsigned NOT NULL,
-  pt_reason varbinary(767),
+  pt_reason varbinary(767) default '', -- Deprecated.
+  pt_reason_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that pt_reason should be used)
   pt_timestamp binary(14) NOT NULL,
   pt_expiry varbinary(14) NOT NULL default '',
   pt_create_perm varbinary(60) NOT NULL
index a520bdb..81bfb47 100644 (file)
                                }
                        } );
 
-                       return $.extend( true, {}, savedParams, savedHighlights );
+                       return $.extend( true, {}, savedParams, savedHighlights, { invert: String( Number( data.invert || 0 ) ) } );
                }
 
                return this.filtersModel.getDefaultParams();
index 47ecfe4..640bbff 100644 (file)
@@ -1,7 +1,7 @@
 
 button_italic.png
 -------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
+Source : https://commons.wikimedia.org/wiki/Image:Button_S_italic.png
 License: Public domain
-Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
+Author : Purodha Blissenbach, https://ksh.wikipedia.org/wiki/User:Purodha
 
index 97c3c05..0dab130 100644 (file)
@@ -182,13 +182,18 @@ class ParserTestRunner {
                        if ( !file_exists( $dir ) ) {
                                continue;
                        }
+                       $counter = 1;
                        $dirIterator = new RecursiveIteratorIterator(
                                new RecursiveDirectoryIterator( $dir )
                        );
                        foreach ( $dirIterator as $fileInfo ) {
                                /** @var SplFileInfo $fileInfo */
                                if ( substr( $fileInfo->getFilename(), -4 ) === '.txt' ) {
-                                       $files[] = $fileInfo->getPathname();
+                                       $name = $info['name'] . $counter;
+                                       while ( isset( $files[$name] ) ) {
+                                               $name = $info['name'] . '_' . $counter++;
+                                       }
+                                       $files[$name] = $fileInfo->getPathname();
                                }
                        }
                }
index ed1f1ae..c844e13 100644 (file)
@@ -1303,13 +1303,17 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        private function resetDB( $db, $tablesUsed ) {
                if ( $db ) {
                        $userTables = [ 'user', 'user_groups', 'user_properties' ];
-                       $coreDBDataTables = array_merge( $userTables, [ 'page', 'revision' ] );
+                       $pageTables = [ 'page', 'revision', 'revision_comment_temp', 'comment' ];
+                       $coreDBDataTables = array_merge( $userTables, $pageTables );
 
-                       // If any of the user tables were marked as used, we should clear all of them.
+                       // If any of the user or page tables were marked as used, we should clear all of them.
                        if ( array_intersect( $tablesUsed, $userTables ) ) {
                                $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
                                TestUserRegistry::clear();
                        }
+                       if ( array_intersect( $tablesUsed, $pageTables ) ) {
+                               $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
+                       }
 
                        $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
                        foreach ( $tablesUsed as $tbl ) {
diff --git a/tests/phpunit/includes/CommentStoreTest.php b/tests/phpunit/includes/CommentStoreTest.php
new file mode 100644 (file)
index 0000000..6dd0925
--- /dev/null
@@ -0,0 +1,614 @@
+<?php
+
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Database
+ * @covers CommentStore
+ * @covers CommentStoreComment
+ */
+class CommentStoreTest extends MediaWikiLangTestCase {
+
+       protected $tablesUsed = [
+               'revision',
+               'revision_comment_temp',
+               'ipblocks',
+               'comment',
+       ];
+
+       /**
+        * Create a store for a particular stage
+        * @param int $stage
+        * @param string $key
+        * @return CommentStore
+        */
+       protected function makeStore( $stage, $key ) {
+               $store = new CommentStore( $key );
+               TestingAccessWrapper::newFromObject( $store )->stage = $stage;
+               return $store;
+       }
+
+       /**
+        * @dataProvider provideGetFields
+        * @param int $stage
+        * @param string $key
+        * @param array $expect
+        */
+       public function testGetFields( $stage, $key, $expect ) {
+               $store = $this->makeStore( $stage, $key );
+               $result = $store->getFields();
+               $this->assertEquals( $expect, $result );
+       }
+
+       public static function provideGetFields() {
+               return [
+                       'Simple table, old' => [
+                               MIGRATION_OLD, 'ipb_reason',
+                               [ 'ipb_reason_text' => 'ipb_reason', 'ipb_reason_data' => 'NULL', 'ipb_reason_cid' => 'NULL' ],
+                       ],
+                       'Simple table, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'ipb_reason',
+                               [ 'ipb_reason_old' => 'ipb_reason', 'ipb_reason_id' => 'ipb_reason_id' ],
+                       ],
+                       'Simple table, write-new' => [
+                               MIGRATION_WRITE_NEW, 'ipb_reason',
+                               [ 'ipb_reason_old' => 'ipb_reason', 'ipb_reason_id' => 'ipb_reason_id' ],
+                       ],
+                       'Simple table, new' => [
+                               MIGRATION_NEW, 'ipb_reason',
+                               [ 'ipb_reason_id' => 'ipb_reason_id' ],
+                       ],
+
+                       'Revision, old' => [
+                               MIGRATION_OLD, 'rev_comment',
+                               [
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                               ],
+                       ],
+                       'Revision, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'rev_comment',
+                               [ 'rev_comment_old' => 'rev_comment', 'rev_comment_pk' => 'rev_id' ],
+                       ],
+                       'Revision, write-new' => [
+                               MIGRATION_WRITE_NEW, 'rev_comment',
+                               [ 'rev_comment_old' => 'rev_comment', 'rev_comment_pk' => 'rev_id' ],
+                       ],
+                       'Revision, new' => [
+                               MIGRATION_NEW, 'rev_comment',
+                               [ 'rev_comment_pk' => 'rev_id' ],
+                       ],
+
+                       'Image, old' => [
+                               MIGRATION_OLD, 'img_description',
+                               [
+                                       'img_description_text' => 'img_description',
+                                       'img_description_data' => 'NULL',
+                                       'img_description_cid' => 'NULL',
+                               ],
+                       ],
+                       'Image, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'img_description',
+                               [ 'img_description_old' => 'img_description', 'img_description_pk' => 'img_name' ],
+                       ],
+                       'Image, write-new' => [
+                               MIGRATION_WRITE_NEW, 'img_description',
+                               [ 'img_description_old' => 'img_description', 'img_description_pk' => 'img_name' ],
+                       ],
+                       'Image, new' => [
+                               MIGRATION_NEW, 'img_description',
+                               [ 'img_description_pk' => 'img_name' ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetJoin
+        * @param int $stage
+        * @param string $key
+        * @param array $expect
+        */
+       public function testGetJoin( $stage, $key, $expect ) {
+               $store = $this->makeStore( $stage, $key );
+               $result = $store->getJoin();
+               $this->assertEquals( $expect, $result );
+       }
+
+       public static function provideGetJoin() {
+               return [
+                       'Simple table, old' => [
+                               MIGRATION_OLD, 'ipb_reason', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'ipb_reason_text' => 'ipb_reason',
+                                               'ipb_reason_data' => 'NULL',
+                                               'ipb_reason_cid' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Simple table, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'ipb_reason', [
+                                       'tables' => [ 'comment_ipb_reason' => 'comment' ],
+                                       'fields' => [
+                                               'ipb_reason_text' => 'COALESCE( comment_ipb_reason.comment_text, ipb_reason )',
+                                               'ipb_reason_data' => 'comment_ipb_reason.comment_data',
+                                               'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'comment_ipb_reason' => [ 'LEFT JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Simple table, write-new' => [
+                               MIGRATION_WRITE_NEW, 'ipb_reason', [
+                                       'tables' => [ 'comment_ipb_reason' => 'comment' ],
+                                       'fields' => [
+                                               'ipb_reason_text' => 'COALESCE( comment_ipb_reason.comment_text, ipb_reason )',
+                                               'ipb_reason_data' => 'comment_ipb_reason.comment_data',
+                                               'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'comment_ipb_reason' => [ 'LEFT JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Simple table, new' => [
+                               MIGRATION_NEW, 'ipb_reason', [
+                                       'tables' => [ 'comment_ipb_reason' => 'comment' ],
+                                       'fields' => [
+                                               'ipb_reason_text' => 'comment_ipb_reason.comment_text',
+                                               'ipb_reason_data' => 'comment_ipb_reason.comment_data',
+                                               'ipb_reason_cid' => 'comment_ipb_reason.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'comment_ipb_reason' => [ 'JOIN', 'comment_ipb_reason.comment_id = ipb_reason_id' ],
+                                       ],
+                               ],
+                       ],
+
+                       'Revision, old' => [
+                               MIGRATION_OLD, 'rev_comment', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'rev_comment_text' => 'rev_comment',
+                                               'rev_comment_data' => 'NULL',
+                                               'rev_comment_cid' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Revision, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'rev_comment', [
+                                       'tables' => [
+                                               'temp_rev_comment' => 'revision_comment_temp',
+                                               'comment_rev_comment' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
+                                               'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                               'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_rev_comment' => [ 'LEFT JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment' => [ 'LEFT JOIN',
+                                                       'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Revision, write-new' => [
+                               MIGRATION_WRITE_NEW, 'rev_comment', [
+                                       'tables' => [
+                                               'temp_rev_comment' => 'revision_comment_temp',
+                                               'comment_rev_comment' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
+                                               'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                               'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_rev_comment' => [ 'LEFT JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment' => [ 'LEFT JOIN',
+                                                       'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Revision, new' => [
+                               MIGRATION_NEW, 'rev_comment', [
+                                       'tables' => [
+                                               'temp_rev_comment' => 'revision_comment_temp',
+                                               'comment_rev_comment' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'rev_comment_text' => 'comment_rev_comment.comment_text',
+                                               'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                               'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment' => [ 'JOIN',
+                                                       'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                                       ],
+                               ],
+                       ],
+
+                       'Image, old' => [
+                               MIGRATION_OLD, 'img_description', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'img_description_text' => 'img_description',
+                                               'img_description_data' => 'NULL',
+                                               'img_description_cid' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Image, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'img_description', [
+                                       'tables' => [
+                                               'temp_img_description' => 'image_comment_temp',
+                                               'comment_img_description' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'img_description_text' => 'COALESCE( comment_img_description.comment_text, img_description )',
+                                               'img_description_data' => 'comment_img_description.comment_data',
+                                               'img_description_cid' => 'comment_img_description.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_img_description' => [ 'LEFT JOIN', 'temp_img_description.imgcomment_name = img_name' ],
+                                               'comment_img_description' => [ 'LEFT JOIN',
+                                                       'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Image, write-new' => [
+                               MIGRATION_WRITE_NEW, 'img_description', [
+                                       'tables' => [
+                                               'temp_img_description' => 'image_comment_temp',
+                                               'comment_img_description' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'img_description_text' => 'COALESCE( comment_img_description.comment_text, img_description )',
+                                               'img_description_data' => 'comment_img_description.comment_data',
+                                               'img_description_cid' => 'comment_img_description.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_img_description' => [ 'LEFT JOIN', 'temp_img_description.imgcomment_name = img_name' ],
+                                               'comment_img_description' => [ 'LEFT JOIN',
+                                                       'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Image, new' => [
+                               MIGRATION_NEW, 'img_description', [
+                                       'tables' => [
+                                               'temp_img_description' => 'image_comment_temp',
+                                               'comment_img_description' => 'comment',
+                                       ],
+                                       'fields' => [
+                                               'img_description_text' => 'comment_img_description.comment_text',
+                                               'img_description_data' => 'comment_img_description.comment_data',
+                                               'img_description_cid' => 'comment_img_description.comment_id',
+                                       ],
+                                       'joins' => [
+                                               'temp_img_description' => [ 'JOIN', 'temp_img_description.imgcomment_name = img_name' ],
+                                               'comment_img_description' => [ 'JOIN',
+                                                       'comment_img_description.comment_id = temp_img_description.imgcomment_description_id' ],
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       private function assertComment( $expect, $actual, $from ) {
+               $this->assertSame( $expect['text'], $actual->text, "text $from" );
+               $this->assertInstanceOf( get_class( $expect['message'] ), $actual->message,
+                       "message class $from" );
+               $this->assertSame( $expect['message']->getKeysToTry(), $actual->message->getKeysToTry(),
+                       "message keys $from" );
+               $this->assertEquals( $expect['message']->text(), $actual->message->text(),
+                       "message rendering $from" );
+               $this->assertEquals( $expect['data'], $actual->data, "data $from" );
+       }
+
+       /**
+        * @dataProvider provideInsertRoundTrip
+        * @param string $table
+        * @param string $key
+        * @param string $pk
+        * @param string $extraFields
+        * @param string|Message $comment
+        * @param array|null $data
+        * @param array $expect
+        */
+       public function testInsertRoundTrip( $table, $key, $pk, $extraFields, $comment, $data, $expect ) {
+               $expectOld = [
+                       'text' => $expect['text'],
+                       'message' => new RawMessage( '$1', [ $expect['text'] ] ),
+                       'data' => null,
+               ];
+
+               $stages = [
+                       MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
+                       MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_NEW ],
+                       MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
+                       MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
+               ];
+
+               foreach ( $stages as $writeStage => $readRange ) {
+                       if ( $key === 'ipb_reason' ) {
+                               $extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
+                       }
+
+                       $wstore = $this->makeStore( $writeStage, $key );
+                       $usesTemp = $key === 'rev_comment';
+
+                       if ( $usesTemp ) {
+                               list( $fields, $callback ) = $wstore->insertWithTempTable( $this->db, $comment, $data );
+                       } else {
+                               $fields = $wstore->insert( $this->db, $comment, $data );
+                       }
+
+                       if ( $writeStage <= MIGRATION_WRITE_BOTH ) {
+                               $this->assertSame( $expect['text'], $fields[$key], "old field, stage=$writeStage" );
+                       } else {
+                               $this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" );
+                       }
+                       if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) {
+                               $this->assertArrayHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
+                       } else {
+                               $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
+                       }
+
+                       $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
+                       $id = $this->db->insertId();
+                       if ( $usesTemp ) {
+                               $callback( $id );
+                       }
+
+                       for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) {
+                               $rstore = $this->makeStore( $readStage, $key );
+
+                               $fieldRow = $this->db->selectRow(
+                                       $table,
+                                       $rstore->getFields(),
+                                       [ $pk => $id ],
+                                       __METHOD__
+                               );
+
+                               $queryInfo = $rstore->getJoin();
+                               $joinRow = $this->db->selectRow(
+                                       [ $table ] + $queryInfo['tables'],
+                                       $queryInfo['fields'],
+                                       [ $pk => $id ],
+                                       __METHOD__,
+                                       [],
+                                       $queryInfo['joins']
+                               );
+
+                               $this->assertComment(
+                                       $writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect,
+                                       $rstore->getCommentLegacy( $this->db, $fieldRow ),
+                                       "w=$writeStage, r=$readStage, from getFields()"
+                               );
+                               $this->assertComment(
+                                       $writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect,
+                                       $rstore->getComment( $joinRow ),
+                                       "w=$writeStage, r=$readStage, from getJoin()"
+                               );
+                       }
+               }
+       }
+
+       public static function provideInsertRoundTrip() {
+               $msgComment = new Message( 'parentheses', [ 'message comment' ] );
+               $textCommentMsg = new RawMessage( '$1', [ 'text comment' ] );
+               $nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] );
+               $ipbfields = [
+                       'ipb_range_start' => '',
+                       'ipb_range_end' => '',
+               ];
+               $revfields = [
+                       'rev_page' => 42,
+                       'rev_text_id' => 42,
+                       'rev_len' => 0,
+               ];
+               $comStoreComment = new CommentStoreComment(
+                       null, 'comment store comment', null, [ 'foo' => 'bar' ]
+               );
+
+               return [
+                       'Simple table, text comment' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', null, [
+                                       'text' => 'text comment',
+                                       'message' => $textCommentMsg,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Simple table, text comment with data' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', [ 'message' => 42 ], [
+                                       'text' => 'text comment',
+                                       'message' => $textCommentMsg,
+                                       'data' => [ 'message' => 42 ],
+                               ]
+                       ],
+                       'Simple table, message comment' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, null, [
+                                       'text' => '(message comment)',
+                                       'message' => $msgComment,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Simple table, message comment with data' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, [ 'message' => 42 ], [
+                                       'text' => '(message comment)',
+                                       'message' => $msgComment,
+                                       'data' => [ 'message' => 42 ],
+                               ]
+                       ],
+                       'Simple table, nested message comment' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $nestedMsgComment, null, [
+                                       'text' => '(Main Page)',
+                                       'message' => $nestedMsgComment,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Simple table, CommentStoreComment' => [
+                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
+                                       'text' => 'comment store comment',
+                                       'message' => $comStoreComment->message,
+                                       'data' => [ 'foo' => 'bar' ],
+                               ]
+                       ],
+
+                       'Revision, text comment' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', null, [
+                                       'text' => 'text comment',
+                                       'message' => $textCommentMsg,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Revision, text comment with data' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', [ 'message' => 42 ], [
+                                       'text' => 'text comment',
+                                       'message' => $textCommentMsg,
+                                       'data' => [ 'message' => 42 ],
+                               ]
+                       ],
+                       'Revision, message comment' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, null, [
+                                       'text' => '(message comment)',
+                                       'message' => $msgComment,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Revision, message comment with data' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, [ 'message' => 42 ], [
+                                       'text' => '(message comment)',
+                                       'message' => $msgComment,
+                                       'data' => [ 'message' => 42 ],
+                               ]
+                       ],
+                       'Revision, nested message comment' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, $nestedMsgComment, null, [
+                                       'text' => '(Main Page)',
+                                       'message' => $nestedMsgComment,
+                                       'data' => null,
+                               ]
+                       ],
+                       'Revision, CommentStoreComment' => [
+                               'revision', 'rev_comment', 'rev_id', $revfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
+                                       'text' => 'comment store comment',
+                                       'message' => $comStoreComment->message,
+                                       'data' => [ 'foo' => 'bar' ],
+                               ]
+                       ],
+               ];
+       }
+
+       public function testGetCommentErrors() {
+               MediaWiki\suppressWarnings();
+               $reset = new ScopedCallback( 'MediaWiki\restoreWarnings' );
+
+               $store = $this->makeStore( MIGRATION_OLD, 'dummy' );
+               $res = $store->getComment( [ 'dummy' => 'comment' ] );
+               $this->assertSame( '', $res->text );
+               $res = $store->getComment( [ 'dummy' => 'comment' ], true );
+               $this->assertSame( 'comment', $res->text );
+
+               $store = $this->makeStore( MIGRATION_NEW, 'dummy' );
+               try {
+                       $store->getComment( [ 'dummy' => 'comment' ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame( '$row does not contain fields needed for comment dummy', $ex->getMessage() );
+               }
+               $res = $store->getComment( [ 'dummy' => 'comment' ], true );
+               $this->assertSame( 'comment', $res->text );
+               try {
+                       $store->getComment( [ 'dummy_id' => 1 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$row does not contain fields needed for comment dummy and getComment(), '
+                               . 'but does have fields for getCommentLegacy()',
+                               $ex->getMessage()
+                       );
+               }
+
+               $store = $this->makeStore( MIGRATION_NEW, 'rev_comment' );
+               try {
+                       $store->getComment( [ 'rev_comment' => 'comment' ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$row does not contain fields needed for comment rev_comment', $ex->getMessage()
+                       );
+               }
+               $res = $store->getComment( [ 'rev_comment' => 'comment' ], true );
+               $this->assertSame( 'comment', $res->text );
+               try {
+                       $store->getComment( [ 'rev_comment_pk' => 1 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$row does not contain fields needed for comment rev_comment and getComment(), '
+                               . 'but does have fields for getCommentLegacy()',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public static function provideStages() {
+               return [
+                       'MIGRATION_OLD' => [ MIGRATION_OLD ],
+                       'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH ],
+                       'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW ],
+                       'MIGRATION_NEW' => [ MIGRATION_NEW ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage Must use insertWithTempTable() for rev_comment
+        */
+       public function testInsertWrong( $stage ) {
+               $store = $this->makeStore( $stage, 'rev_comment' );
+               $store->insert( $this->db, 'foo' );
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage Must use insert() for ipb_reason
+        */
+       public function testInsertWithTempTableWrong( $stage ) {
+               $store = $this->makeStore( $stage, 'ipb_reason' );
+               $store->insertWithTempTable( $this->db, 'foo' );
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        */
+       public function testInsertWithTempTableDeprecated( $stage ) {
+               $wrap = TestingAccessWrapper::newFromClass( CommentStore::class );
+               $wrap->formerTempTables += [ 'ipb_reason' => '1.30' ];
+
+               $this->hideDeprecated( 'CommentStore::insertWithTempTable for ipb_reason' );
+               $store = $this->makeStore( $stage, 'ipb_reason' );
+               list( $fields, $callback ) = $store->insertWithTempTable( $this->db, 'foo' );
+               $this->assertTrue( is_callable( $callback ) );
+       }
+
+       public function testConstructor() {
+               $this->assertInstanceOf( CommentStore::class, CommentStore::newKey( 'dummy' ) );
+       }
+
+}
index 3ba82a6..b207e06 100644 (file)
@@ -146,7 +146,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $orig = $this->makeRevision();
 
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'revision', '*', [ 'rev_id' => $orig->getId() ] );
+               $res = $dbr->select( 'revision', Revision::selectFields(), [ 'rev_id' => $orig->getId() ] );
                $this->assertTrue( is_object( $res ), 'query failed' );
 
                $row = $res->fetchObject();
@@ -164,7 +164,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $orig = $this->makeRevision();
 
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'revision', '*', [ 'rev_id' => $orig->getId() ] );
+               $res = $dbr->select( 'revision', Revision::selectFields(), [ 'rev_id' => $orig->getId() ] );
                $this->assertTrue( is_object( $res ), 'query failed' );
 
                $row = $res->fetchObject();
@@ -188,7 +188,9 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
 
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'archive', '*', [ 'ar_rev_id' => $orig->getId() ] );
+               $res = $dbr->select(
+                       'archive', Revision::selectArchiveFields(), [ 'ar_rev_id' => $orig->getId() ]
+               );
                $this->assertTrue( is_object( $res ), 'query failed' );
 
                $row = $res->fetchObject();
index 4744875..62ba5f6 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use Wikimedia\ScopedCallback;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -447,100 +448,175 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
                                null,
+                               [],
                                [ 'rc_type', 'rc_minor', 'rc_bot' ],
                                [],
                                [],
+                               [],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
                                null,
+                               [],
                                [ 'rc_user_text' ],
                                [],
                                [],
+                               [],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
                                null,
+                               [],
                                [ 'rc_user' ],
                                [],
                                [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [],
+                               [
+                                       'rc_comment_text' => 'rc_comment',
+                                       'rc_comment_data' => 'NULL',
+                                       'rc_comment_cid' => 'NULL',
+                               ],
+                               [],
+                               [],
+                               [],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
+                               [],
+                               [],
+                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
+                               [],
+                               [],
+                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW ],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
                                null,
-                               [ 'rc_comment' ],
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'comment_rc_comment.comment_text',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
                                [],
                                [],
+                               [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW ],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
                                null,
+                               [],
                                [ 'rc_patrolled', 'rc_log_type' ],
                                [],
                                [],
+                               [],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
                                null,
+                               [],
                                [ 'rc_old_len', 'rc_new_len' ],
                                [],
                                [],
+                               [],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
                                null,
+                               [],
                                [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
                                [],
                                [],
+                               [],
                        ],
                        [
                                [ 'namespaceIds' => [ 0, 1 ] ],
                                null,
                                [],
+                               [],
                                [ 'wl_namespace' => [ 0, 1 ] ],
                                [],
+                               [],
                        ],
                        [
                                [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
                                null,
                                [],
+                               [],
                                [ 'wl_namespace' => [ 0, 1 ] ],
                                [],
+                               [],
                        ],
                        [
                                [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
                                [],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
                                null,
                                [],
                                [],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
                                null,
                                [],
                                [],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp <= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                        [
                                [
@@ -550,22 +626,28 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
                                ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp <= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
                        ],
                        [
                                [
@@ -575,133 +657,169 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
                                ],
                                null,
                                [],
+                               [],
                                [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
                        ],
                        [
                                [ 'limit' => 10 ],
                                null,
                                [],
                                [],
+                               [],
                                [ 'LIMIT' => 11 ],
+                               [],
                        ],
                        [
                                [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
                                null,
                                [],
                                [],
+                               [],
                                [ 'LIMIT' => 11 ],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_minor != 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_minor = 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_bot != 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_bot = 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_user = 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_user != 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_patrolled != 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_patrolled = 0' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
                                null,
                                [],
+                               [],
                                [ 'rc_timestamp >= wl_notificationtimestamp' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
                                null,
                                [],
+                               [],
                                [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'onlyByUser' => 'SomeOtherUser' ],
                                null,
                                [],
+                               [],
                                [ 'rc_user_text' => 'SomeOtherUser' ],
                                [],
+                               [],
                        ],
                        [
                                [ 'notByUser' => 'SomeOtherUser' ],
                                null,
                                [],
+                               [],
                                [ "rc_user_text != 'SomeOtherUser'" ],
                                [],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
                                [ '20151212010101', 123 ],
                                [],
+                               [],
                                [
                                        "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
                                ],
                                [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
                                [ '20151212010101', 123 ],
                                [],
+                               [],
                                [
                                        "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
                                ],
                                [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
                                [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
                                [],
+                               [],
                                [
                                        "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
                                ],
                                [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
                        ],
                ];
        }
@@ -712,10 +830,28 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
        public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
                array $options,
                $startFrom,
+               array $expectedExtraTables,
                array $expectedExtraFields,
                array $expectedExtraConds,
-               array $expectedDbOptions
+               array $expectedDbOptions,
+               array $expectedExtraJoinConds,
+               array $globals = []
        ) {
+               // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals().
+               if ( $globals ) {
+                       $resetGlobals = [];
+                       foreach ( $globals as $k => $v ) {
+                               $resetGlobals[$k] = $GLOBALS[$k];
+                               $GLOBALS[$k] = $v;
+                       }
+                       $reset = new ScopedCallback( function () use ( $resetGlobals ) {
+                               foreach ( $resetGlobals as $k => $v ) {
+                                       $GLOBALS[$k] = $v;
+                               }
+                       } );
+               }
+
+               $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
                $expectedFields = array_merge(
                        [
                                'rc_id',
@@ -736,29 +872,33 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
                        [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
                        $expectedExtraConds
                );
+               $expectedJoinConds = array_merge(
+                       [
+                               'watchlist' => [
+                                       'INNER JOIN',
+                                       [
+                                               'wl_namespace=rc_namespace',
+                                               'wl_title=rc_title'
+                                       ]
+                               ],
+                               'page' => [
+                                       'LEFT JOIN',
+                                       'rc_cur_id=page_id',
+                               ],
+                       ],
+                       $expectedExtraJoinConds
+               );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
                        ->method( 'select' )
                        ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $expectedTables,
                                $expectedFields,
                                $expectedConds,
                                $this->isType( 'string' ),
                                $expectedDbOptions,
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                                       'page' => [
-                                               'LEFT JOIN',
-                                               'rc_cur_id=page_id',
-                                       ],
-                               ]
+                               $expectedJoinConds
                        )
                        ->will( $this->returnValue( [] ) );
 
index 4f4453f..fdbeded 100644 (file)
@@ -1074,6 +1074,8 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
                        'rc_user' => 0,
                        'rc_user_text' => 'External User',
                        'rc_comment' => '',
+                       'rc_comment_text' => '',
+                       'rc_comment_data' => null,
                        'rc_this_oldid' => $title->getLatestRevID(),
                        'rc_last_oldid' => $title->getLatestRevID(),
                        'rc_bot' => 0,
index 68f9079..d638d0f 100644 (file)
@@ -31,6 +31,8 @@ class RecentChangeTest extends MediaWikiTestCase {
                $row->rc_foo = 'AAA';
                $row->rc_timestamp = '20150921134808';
                $row->rc_deleted = 'bar';
+               $row->rc_comment_text = 'comment';
+               $row->rc_comment_data = null;
 
                $rc = RecentChange::newFromRow( $row );
 
@@ -38,6 +40,29 @@ class RecentChangeTest extends MediaWikiTestCase {
                        'rc_foo' => 'AAA',
                        'rc_timestamp' => '20150921134808',
                        'rc_deleted' => 'bar',
+                       'rc_comment' => 'comment',
+                       'rc_comment_text' => 'comment',
+                       'rc_comment_data' => null,
+               ];
+               $this->assertEquals( $expected, $rc->getAttributes() );
+
+               $row = new stdClass();
+               $row->rc_foo = 'AAA';
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 'bar';
+               $row->rc_comment = 'comment';
+
+               MediaWiki\suppressWarnings();
+               $rc = RecentChange::newFromRow( $row );
+               MediaWiki\restoreWarnings();
+
+               $expected = [
+                       'rc_foo' => 'AAA',
+                       'rc_timestamp' => '20150921134808',
+                       'rc_deleted' => 'bar',
+                       'rc_comment' => 'comment',
+                       'rc_comment_text' => 'comment',
+                       'rc_comment_data' => null,
                ];
                $this->assertEquals( $expected, $rc->getAttributes() );
        }
index 4da09d8..2c30948 100644 (file)
@@ -119,6 +119,8 @@ class TestRecentChangesHelper {
                                'rc_last_oldid' => $lastid,
                                'rc_cur_id' => $curid,
                                'rc_comment' => '[[:Testpage]] added to category',
+                               'rc_comment_text' => '[[:Testpage]] added to category',
+                               'rc_comment_data' => null,
                                'rc_old_len' => 0,
                                'rc_new_len' => 0,
                        ]
@@ -139,6 +141,8 @@ class TestRecentChangesHelper {
                        'rc_old_len' => 212,
                        'rc_new_len' => 188,
                        'rc_comment' => '',
+                       'rc_comment_text' => '',
+                       'rc_comment_data' => null,
                        'rc_minor' => 0,
                        'rc_bot' => 0,
                        'rc_type' => 0,
index 639c323..4c0a5fa 100644 (file)
@@ -378,16 +378,33 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
        protected function assertRecentChangeByCategorization(
                Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows
        ) {
-               $this->assertSelect(
-                       'recentchanges',
-                       'rc_title, rc_comment',
-                       [
-                               'rc_type' => RC_CATEGORIZE,
-                               'rc_namespace' => NS_CATEGORY,
-                               'rc_title' => $categoryTitle->getDBkey()
-                       ],
-                       $expectedRows
-               );
+               global $wgCommentTableSchemaMigrationStage;
+
+               if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       $this->assertSelect(
+                               'recentchanges',
+                               'rc_title, rc_comment',
+                               [
+                                       'rc_type' => RC_CATEGORIZE,
+                                       'rc_namespace' => NS_CATEGORY,
+                                       'rc_title' => $categoryTitle->getDBkey()
+                               ],
+                               $expectedRows
+                       );
+               }
+               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       $this->assertSelect(
+                               [ 'recentchanges', 'comment' ],
+                               'rc_title, comment_text',
+                               [
+                                       'rc_type' => RC_CATEGORIZE,
+                                       'rc_namespace' => NS_CATEGORY,
+                                       'rc_title' => $categoryTitle->getDBkey(),
+                                       'comment_id = rc_comment_id',
+                               ],
+                               $expectedRows
+                       );
+               }
        }
 
        private function runAllRelatedJobs() {
index c289839..2dc9a2c 100644 (file)
@@ -39,7 +39,8 @@ abstract class LogFormatterTestCase extends MediaWikiLangTestCase {
                        'log_namespace' => isset( $data['namespace'] ) ? $data['namespace'] : NS_MAIN,
                        'log_title' => isset( $data['title'] ) ? $data['title'] : 'Main_Page',
                        'log_page' => isset( $data['page'] ) ? $data['page'] : 0,
-                       'log_comment' => isset( $data['comment'] ) ? $data['comment'] : '',
+                       'log_comment_text' => isset( $data['comment'] ) ? $data['comment'] : '',
+                       'log_comment_data' => null,
                        'log_params' => $legacy
                                ? LogPage::makeParamBlob( $data['params'] )
                                : LogEntryBase::makeParamBlob( $data['params'] ),
index a9f74b6..d0fefde 100644 (file)
@@ -17,6 +17,7 @@ class WikiPageTest extends MediaWikiLangTestCase {
                        $this->tablesUsed,
                        [ 'page',
                                'revision',
+                               'archive',
                                'text',
 
                                'recentchanges',
@@ -1123,4 +1124,84 @@ more stuff
                $page = WikiPage::factory( $title );
                $this->assertEquals( 'WikiPage', get_class( $page ) );
        }
+
+       /**
+        * @dataProvider provideCommentMigrationOnDeletion
+        * @param int $wstage
+        * @param int $rstage
+        */
+       public function testCommentMigrationOnDeletion( $wstage, $rstage ) {
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $wstage );
+               $dbr = wfGetDB( DB_REPLICA );
+
+               $page = $this->createPage(
+                       "WikiPageTest_testCommentMigrationOnDeletion",
+                       "foo",
+                       CONTENT_MODEL_WIKITEXT
+               );
+               $revid = $page->getLatest();
+               if ( $wstage > MIGRATION_OLD ) {
+                       $comment_id = $dbr->selectField(
+                               'revision_comment_temp',
+                               'revcomment_comment_id',
+                               [ 'revcomment_rev' => $revid ],
+                               __METHOD__
+                       );
+               }
+
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $rstage );
+
+               $page->doDeleteArticle( "testing deletion" );
+
+               if ( $rstage > MIGRATION_OLD ) {
+                       // Didn't leave behind any 'revision_comment_temp' rows
+                       $n = $dbr->selectField(
+                               'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__
+                       );
+                       $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' );
+
+                       // Copied or upgraded the comment_id, as applicable
+                       $ar_comment_id = $dbr->selectField(
+                               'archive',
+                               'ar_comment_id',
+                               [ 'ar_rev_id' => $revid ],
+                               __METHOD__
+                       );
+                       if ( $wstage > MIGRATION_OLD ) {
+                               $this->assertSame( $comment_id, $ar_comment_id );
+                       } else {
+                               $this->assertNotEquals( 0, $ar_comment_id );
+                       }
+               }
+
+               // Copied rev_comment, if applicable
+               if ( $rstage <= MIGRATION_WRITE_BOTH && $wstage <= MIGRATION_WRITE_BOTH ) {
+                       $ar_comment = $dbr->selectField(
+                               'archive',
+                               'ar_comment',
+                               [ 'ar_rev_id' => $revid ],
+                               __METHOD__
+                       );
+                       $this->assertSame( 'testing', $ar_comment );
+               }
+       }
+
+       public static function provideCommentMigrationOnDeletion() {
+               return [
+                       [ MIGRATION_OLD, MIGRATION_OLD ],
+                       [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ],
+                       [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
+                       [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ],
+                       [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ],
+                       [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ],
+                       [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
+                       [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ],
+                       [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ],
+                       [ MIGRATION_WRITE_NEW, MIGRATION_NEW ],
+                       [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ],
+                       [ MIGRATION_NEW, MIGRATION_WRITE_NEW ],
+                       [ MIGRATION_NEW, MIGRATION_NEW ],
+               ];
+       }
+
 }
index 09052f3..4ea1090 100644 (file)
@@ -82,15 +82,15 @@ class ParserTestTopLevelSuite extends PHPUnit_Framework_TestSuite {
 
                # Filter out .txt files
                $files = ParserTestRunner::getParserTestFiles();
-               foreach ( $files as $parserTestFile ) {
+               foreach ( $files as $extName => $parserTestFile ) {
                        $isCore = ( 0 === strpos( $parserTestFile, $mwTestDir ) );
 
                        if ( $isCore && $wantsCore ) {
                                self::debug( "included core parser tests: $parserTestFile" );
-                               $filesToTest[] = $parserTestFile;
+                               $filesToTest[$extName] = $parserTestFile;
                        } elseif ( !$isCore && $wantsRest ) {
                                self::debug( "included non core parser tests: $parserTestFile" );
-                               $filesToTest[] = $parserTestFile;
+                               $filesToTest[$extName] = $parserTestFile;
                        } else {
                                self::debug( "skipped parser tests: $parserTestFile" );
                        }
@@ -100,12 +100,13 @@ class ParserTestTopLevelSuite extends PHPUnit_Framework_TestSuite {
 
                $testList = [];
                $counter = 0;
-               foreach ( $filesToTest as $fileName ) {
-                       // Call the highest level directory the extension name.
-                       // It may or may not actually be, but it should be close
-                       // enough to cause there to be separate names for different
-                       // things, which is good enough for our purposes.
-                       $extensionName = basename( dirname( $fileName ) );
+               foreach ( $filesToTest as $extensionName => $fileName ) {
+                       if ( is_int( $extensionName ) ) {
+                               // If there's no extension name because this is coming
+                               // from the legacy global, then assume the next level directory
+                               // is the extension name (e.g. extensions/FooBar/parserTests.txt).
+                               $extensionName = basename( dirname( $fileName ) );
+                       }
                        $testsName = $extensionName . '__' . basename( $fileName, '.txt' );
                        $parserTestClassName = ucfirst( $testsName );