Merge "Fix fatal error in updateSearchIndex.php script"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 9 Feb 2016 14:25:33 +0000 (14:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 9 Feb 2016 14:25:33 +0000 (14:25 +0000)
58 files changed:
RELEASE-NOTES-1.27
autoload.php
includes/EditPage.php
includes/Linker.php
includes/MergeHistory.php [new file with mode: 0644]
includes/OutputPage.php
includes/actions/EditAction.php
includes/api/ApiMain.php
includes/api/ApiMergeHistory.php [new file with mode: 0644]
includes/api/i18n/en.json
includes/api/i18n/lb.json
includes/api/i18n/qqq.json
includes/api/i18n/zh-hans.json
includes/installer/Installer.php
includes/installer/LocalSettingsGenerator.php
includes/installer/WebInstallerExistingWiki.php
includes/page/WikiPage.php
includes/parser/LinkHolderArray.php
includes/session/BotPasswordSessionProvider.php
includes/session/CookieSessionProvider.php
includes/session/PHPSessionHandler.php
includes/session/SessionBackend.php
includes/session/SessionManager.php
includes/specials/SpecialLog.php
includes/specials/SpecialMergeHistory.php
includes/specials/SpecialUndelete.php
includes/user/User.php
languages/data/Names.php
languages/i18n/ar.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/cu.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/he.json
languages/i18n/ku-latn.json
languages/i18n/la.json
languages/i18n/lb.json
languages/i18n/nb.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/sh.json
languages/i18n/uk.json
languages/messages/MessagesGom.php
languages/messages/MessagesGom_deva.php
resources/src/jquery/jquery.tablesorter.less
resources/src/mediawiki.skinning/content.css
resources/src/mediawiki.skinning/images/sort_both_readonly.png [deleted file]
resources/src/mediawiki.skinning/images/sort_both_readonly.svg [deleted file]
resources/src/mediawiki/mediawiki.checkboxtoggle.js
tests/phpunit/includes/EditPageTest.php
tests/phpunit/includes/MergeHistoryTest.php [new file with mode: 0644]
tests/phpunit/includes/session/BotPasswordSessionProviderTest.php
tests/phpunit/includes/session/CookieSessionProviderTest.php
tests/phpunit/includes/session/PHPSessionHandlerTest.php
tests/phpunit/includes/session/SessionManagerTest.php

index d1f8ca7..04bab03 100644 (file)
@@ -160,6 +160,9 @@ production.
   All values are now allowed for the role attribute.
 * $wgContentHandlers now also supports callbacks to create an instance of the
   appropriate ContentHandler subclass.
+* Added $wgAuthenticationTokenVersion, which if non-null prevents the
+  user_token database field from being exposed in cookies. Setting this would
+  be a good idea, but will log out all current sessions.
 
 === External library changes in 1.27 ===
 
index 4d48de0..937b8c9 100644 (file)
@@ -52,6 +52,7 @@ $wgAutoloadLocalClasses = array(
        'ApiLogout' => __DIR__ . '/includes/api/ApiLogout.php',
        'ApiMain' => __DIR__ . '/includes/api/ApiMain.php',
        'ApiManageTags' => __DIR__ . '/includes/api/ApiManageTags.php',
+       'ApiMergeHistory' => __DIR__ . '/includes/api/ApiMergeHistory.php',
        'ApiMessage' => __DIR__ . '/includes/api/ApiMessage.php',
        'ApiModuleManager' => __DIR__ . '/includes/api/ApiModuleManager.php',
        'ApiMove' => __DIR__ . '/includes/api/ApiMove.php',
@@ -825,6 +826,7 @@ $wgAutoloadLocalClasses = array(
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
        'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
        'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
+       'MergeHistory' => __DIR__ . '/includes/MergeHistory.php',
        'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
        'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php',
index 277a6cc..914bad4 100644 (file)
@@ -2232,8 +2232,6 @@ class EditPage {
                        $wgOut->addModules( 'mediawiki.action.edit.stash' );
                }
 
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
                # Enabled article-related sidebar, toplinks, etc.
                $wgOut->setArticleRelated( true );
 
index 4b9b963..f0a2963 100644 (file)
@@ -188,6 +188,7 @@ class Linker {
         *       Has compatibility issues on some setups, so avoid wherever possible.
         *     'http': Force a full URL with http:// as the scheme.
         *     'https': Force a full URL with https:// as the scheme.
+        *     'stubThreshold' => (int): Stub threshold to use when determining link classes.
         * @return string HTML <a> attribute
         */
        public static function link(
@@ -218,7 +219,7 @@ class Linker {
                $target = self::normaliseSpecialPage( $target );
 
                # If we don't know whether the page exists, let's find out.
-               if ( !in_array( 'known', $options ) && !in_array( 'broken', $options ) ) {
+               if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) {
                        if ( $target->isKnown() ) {
                                $options[] = 'known';
                        } else {
@@ -227,14 +228,14 @@ class Linker {
                }
 
                $oldquery = array();
-               if ( in_array( "forcearticlepath", $options ) && $query ) {
+               if ( in_array( "forcearticlepath", $options, true ) && $query ) {
                        $oldquery = $query;
                        $query = array();
                }
 
                # Note: we want the href attribute first, for prettiness.
                $attribs = array( 'href' => self::linkUrl( $target, $query, $options ) );
-               if ( in_array( 'forcearticlepath', $options ) && $oldquery ) {
+               if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) {
                        $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
                }
 
@@ -277,7 +278,7 @@ class Linker {
        private static function linkUrl( $target, $query, $options ) {
                # We don't want to include fragments for broken links, because they
                # generally make no sense.
-               if ( in_array( 'broken', $options ) && $target->hasFragment() ) {
+               if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) {
                        $target = clone $target;
                        $target->setFragment( '' );
                }
@@ -285,15 +286,15 @@ class Linker {
                # If it's a broken link, add the appropriate query pieces, unless
                # there's already an action specified, or unless 'edit' makes no sense
                # (i.e., for a nonexistent special page).
-               if ( in_array( 'broken', $options ) && empty( $query['action'] )
+               if ( in_array( 'broken', $options, true ) && empty( $query['action'] )
                        && !$target->isSpecialPage() ) {
                        $query['action'] = 'edit';
                        $query['redlink'] = '1';
                }
 
-               if ( in_array( 'http', $options ) ) {
+               if ( in_array( 'http', $options, true ) ) {
                        $proto = PROTO_HTTP;
-               } elseif ( in_array( 'https', $options ) ) {
+               } elseif ( in_array( 'https', $options, true ) ) {
                        $proto = PROTO_HTTPS;
                } else {
                        $proto = PROTO_RELATIVE;
@@ -316,11 +317,11 @@ class Linker {
                global $wgUser;
                $defaults = array();
 
-               if ( !in_array( 'noclasses', $options ) ) {
+               if ( !in_array( 'noclasses', $options, true ) ) {
                        # Now build the classes.
                        $classes = array();
 
-                       if ( in_array( 'broken', $options ) ) {
+                       if ( in_array( 'broken', $options, true ) ) {
                                $classes[] = 'new';
                        }
 
@@ -328,8 +329,11 @@ class Linker {
                                $classes[] = 'extiw';
                        }
 
-                       if ( !in_array( 'broken', $options ) ) { # Avoid useless calls to LinkCache (see r50387)
-                               $colour = self::getLinkColour( $target, $wgUser->getStubThreshold() );
+                       if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387)
+                               $colour = self::getLinkColour(
+                                       $target,
+                                       isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold()
+                               );
                                if ( $colour !== '' ) {
                                        $classes[] = $colour; # mw-redirect or stub
                                }
@@ -343,7 +347,7 @@ class Linker {
                if ( $target->getPrefixedText() == '' ) {
                        # A link like [[#Foo]].  This used to mean an empty title
                        # attribute, but that's silly.  Just don't output a title.
-               } elseif ( in_array( 'known', $options ) ) {
+               } elseif ( in_array( 'known', $options, true ) ) {
                        $defaults['title'] = $target->getPrefixedText();
                } else {
                        // This ends up in parser cache!
@@ -1845,7 +1849,7 @@ class Linker {
                }
 
                $editCount = false;
-               if ( in_array( 'verify', $options ) ) {
+               if ( in_array( 'verify', $options, true ) ) {
                        $editCount = self::getRollbackEditCount( $rev, true );
                        if ( $editCount === false ) {
                                return '';
@@ -1854,7 +1858,7 @@ class Linker {
 
                $inner = self::buildRollbackLink( $rev, $context, $editCount );
 
-               if ( !in_array( 'noBrackets', $options ) ) {
+               if ( !in_array( 'noBrackets', $options, true ) ) {
                        $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
                }
 
diff --git a/includes/MergeHistory.php b/includes/MergeHistory.php
new file mode 100644 (file)
index 0000000..a3861ee
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+
+/**
+ *
+ *
+ * Created on Dec 29, 2015
+ *
+ * Copyright © 2015 Geoffrey Mon <geofbot@gmail.com>
+ *
+ * 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
+ */
+
+/**
+ * Handles the backend logic of merging the histories of two
+ * pages.
+ *
+ * @since 1.27
+ */
+class MergeHistory {
+
+       /** @const int Maximum number of revisions that can be merged at once (avoid too much slave lag) */
+       const REVISION_LIMIT = 5000;
+
+       /** @var Title Page from which history will be merged */
+       protected $source;
+
+       /** @var Title Page to which history will be merged */
+       protected $dest;
+
+       /** @var DatabaseBase Database that we are using */
+       protected $dbw;
+
+       /** @var MWTimestamp Maximum timestamp that we can use (oldest timestamp of dest) */
+       protected $maxTimestamp;
+
+       /** @var string SQL WHERE condition that selects source revisions to insert into destination */
+       protected $timeWhere;
+
+       /** @var MWTimestamp|boolean Timestamp upto which history from the source will be merged */
+       protected $timestampLimit;
+
+       /** @var integer Number of revisions merged (for Special:MergeHistory success message) */
+       protected $revisionsMerged;
+
+       /**
+        * MergeHistory constructor.
+        * @param Title $source Page from which history will be merged
+        * @param Title $dest Page to which history will be merged
+        * @param string|boolean $timestamp Timestamp up to which history from the source will be merged
+        */
+       public function __construct( Title $source, Title $dest, $timestamp = false ) {
+               // Save the parameters
+               $this->source = $source;
+               $this->dest = $dest;
+
+               // Get the database
+               $this->dbw = wfGetDB( DB_MASTER );
+
+               // Max timestamp should be min of destination page
+               $firstDestTimestamp = $this->dbw->selectField(
+                       'revision',
+                       'MIN(rev_timestamp)',
+                       array( 'rev_page' => $this->dest->getArticleID() ),
+                       __METHOD__
+               );
+               $this->maxTimestamp = new MWTimestamp( $firstDestTimestamp );
+
+               // Get the timestamp pivot condition
+               try {
+                       if ( $timestamp ) {
+                               // If we have a requested timestamp, use the
+                               // latest revision up to that point as the insertion point
+                               $mwTimestamp = new MWTimestamp( $timestamp );
+                               $lastWorkingTimestamp = $this->dbw->selectField(
+                                       'revision',
+                                       'MAX(rev_timestamp)',
+                                       array(
+                                               'rev_timestamp <= ' . $this->dbw->timestamp( $mwTimestamp ),
+                                               'rev_page' => $this->source->getArticleID()
+                                       ),
+                                       __METHOD__
+                               );
+                               $mwLastWorkingTimestamp = new MWTimestamp( $lastWorkingTimestamp );
+
+                               $timeInsert = $mwLastWorkingTimestamp;
+                               $this->timestampLimit = $mwLastWorkingTimestamp;
+                       } else {
+                               // If we don't, merge entire source page history into the
+                               // beginning of destination page history
+
+                               // Get the latest timestamp of the source
+                               $lastSourceTimestamp = $this->dbw->selectField(
+                                       array( 'page', 'revision' ),
+                                       'rev_timestamp',
+                                       array( 'page_id' => $this->source->getArticleID(),
+                                               'page_latest = rev_id'
+                                       ),
+                                       __METHOD__
+                               );
+                               $lasttimestamp = new MWTimestamp( $lastSourceTimestamp );
+
+                               $timeInsert = $this->maxTimestamp;
+                               $this->timestampLimit = $lasttimestamp;
+                       }
+
+                       $this->timeWhere = "rev_timestamp <= {$this->dbw->timestamp( $timeInsert )}";
+               } catch ( TimestampException $ex ) {
+                       // The timestamp we got is screwed up and merge cannot continue
+                       // This should be detected by $this->isValidMerge()
+                       $this->timestampLimit = false;
+               }
+       }
+
+       /**
+        * Get the number of revisions that will be moved
+        * @return int
+        */
+       public function getRevisionCount() {
+               $count = $this->dbw->selectRowCount( 'revision', '1',
+                       array( 'rev_page' => $this->source->getArticleID(), $this->timeWhere ),
+                       __METHOD__,
+                       array( 'LIMIT' => self::REVISION_LIMIT + 1 )
+               );
+
+               return $count;
+       }
+
+       /**
+        * Get the number of revisions that were moved
+        * Used in the SpecialMergeHistory success message
+        * @return int
+        */
+       public function getMergedRevisionCount() {
+               return $this->revisionsMerged;
+       }
+
+       /**
+        * Check if the merge is possible
+        * @param User $user
+        * @param string $reason
+        * @return Status
+        */
+       public function checkPermissions( User $user, $reason ) {
+               $status = new Status();
+
+               // Check if user can edit both pages
+               $errors = wfMergeErrorArrays(
+                       $this->source->getUserPermissionsErrors( 'edit', $user ),
+                       $this->dest->getUserPermissionsErrors( 'edit', $user )
+               );
+
+               // Convert into a Status object
+               if ( $errors ) {
+                       foreach ( $errors as $error ) {
+                               call_user_func_array( array( $status, 'fatal' ), $error );
+                       }
+               }
+
+               // Anti-spam
+               if ( EditPage::matchSummarySpamRegex( $reason ) !== false ) {
+                       // This is kind of lame, won't display nice
+                       $status->fatal( 'spamprotectiontext' );
+               }
+
+               // Check mergehistory permission
+               if ( !$user->isAllowed( 'mergehistory' ) ) {
+                       // User doesn't have the right to merge histories
+                       $status->fatal( 'mergehistory-fail-permission' );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Does various sanity checks that the merge is
+        * valid. Only things based on the two pages
+        * should be checked here.
+        *
+        * @return Status
+        */
+       public function isValidMerge() {
+               $status = new Status();
+
+               // If either article ID is 0, then revisions cannot be reliably selected
+               if ( $this->source->getArticleID() === 0 ) {
+                       $status->fatal( 'mergehistory-fail-invalid-source' );
+               }
+               if ( $this->dest->getArticleID() === 0 ) {
+                       $status->fatal( 'mergehistory-fail-invalid-dest' );
+               }
+
+               // Make sure page aren't the same
+               if ( $this->source->equals( $this->dest ) ) {
+                       $status->fatal( 'mergehistory-fail-self-merge' );
+               }
+
+               // Make sure the timestamp is valid
+               if ( !$this->timestampLimit ) {
+                       $status->fatal( 'mergehistory-fail-bad-timestamp' );
+               }
+
+               // $this->timestampLimit must be older than $this->maxTimestamp
+               if ( $this->timestampLimit > $this->maxTimestamp ) {
+                       $status->fatal( 'mergehistory-fail-timestamps-overlap' );
+               }
+
+               // Check that there are not too many revisions to move
+               if ( $this->timestampLimit && $this->getRevisionCount() > self::REVISION_LIMIT ) {
+                       $status->fatal( 'mergehistory-fail-toobig', Message::numParam( self::REVISION_LIMIT ) );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Actually attempt the history move
+        *
+        * @todo if all versions of page A are moved to B and then a user
+        * tries to do a reverse-merge via the "unmerge" log link, then page
+        * A will still be a redirect (as it was after the original merge),
+        * though it will have the old revisions back from before (as expected).
+        * The user may have to "undo" the redirect manually to finish the "unmerge".
+        * Maybe this should delete redirects at the source page of merges?
+        *
+        * @param User $user
+        * @param string $reason
+        * @return Status status of the history merge
+        */
+       public function merge( User $user, $reason = '' ) {
+               $status = new Status();
+
+               // Check validity and permissions required for merge
+               $validCheck = $this->isValidMerge(); // Check this first to check for null pages
+               if ( !$validCheck->isOK() ) {
+                       return $validCheck;
+               }
+               $permCheck = $this->checkPermissions( $user, $reason );
+               if ( !$permCheck->isOK() ) {
+                       return $permCheck;
+               }
+
+               $this->dbw->update(
+                       'revision',
+                       array( 'rev_page' => $this->dest->getArticleID() ),
+                       array( 'rev_page' => $this->source->getArticleID(), $this->timeWhere ),
+                       __METHOD__
+               );
+
+               // Check if this did anything
+               $this->revisionsMerged = $this->dbw->affectedRows();
+               if ( $this->revisionsMerged < 1 ) {
+                       $status->fatal( 'mergehistory-fail-no-change' );
+                       return $status;
+               }
+
+               // Make the source page a redirect if no revisions are left
+               $haveRevisions = $this->dbw->selectField(
+                       'revision',
+                       'rev_timestamp',
+                       array( 'rev_page' => $this->source->getArticleID() ),
+                       __METHOD__,
+                       array( 'FOR UPDATE' )
+               );
+               if ( !$haveRevisions ) {
+                       if ( $reason ) {
+                               $reason = wfMessage(
+                                       'mergehistory-comment',
+                                       $this->source->getPrefixedText(),
+                                       $this->dest->getPrefixedText(),
+                                       $reason
+                               )->inContentLanguage()->text();
+                       } else {
+                               $reason = wfMessage(
+                                       'mergehistory-autocomment',
+                                       $this->source->getPrefixedText(),
+                                       $this->dest->getPrefixedText()
+                               )->inContentLanguage()->text();
+                       }
+
+                       $contentHandler = ContentHandler::getForTitle( $this->source );
+                       $redirectContent = $contentHandler->makeRedirectContent(
+                               $this->dest,
+                               wfMessage( 'mergehistory-redirect-text' )->inContentLanguage()->plain()
+                       );
+
+                       if ( $redirectContent ) {
+                               $redirectPage = WikiPage::factory( $this->source );
+                               $redirectRevision = new Revision( array(
+                                       'title' => $this->source,
+                                       'page' => $this->source->getArticleID(),
+                                       'comment' => $reason,
+                                       'content' => $redirectContent ) );
+                               $redirectRevision->insertOn( $this->dbw );
+                               $redirectPage->updateRevisionOn( $this->dbw, $redirectRevision );
+
+                               // Now, we record the link from the redirect to the new title.
+                               // It should have no other outgoing links...
+                               $this->dbw->delete(
+                                       'pagelinks',
+                                       array( 'pl_from' => $this->dest->getArticleID() ),
+                                       __METHOD__
+                               );
+                               $this->dbw->insert( 'pagelinks',
+                                       array(
+                                               'pl_from' => $this->dest->getArticleID(),
+                                               'pl_from_namespace' => $this->dest->getNamespace(),
+                                               'pl_namespace' => $this->dest->getNamespace(),
+                                               'pl_title' => $this->dest->getDBkey() ),
+                                       __METHOD__
+                               );
+                       } else {
+                               // Warning if we couldn't create the redirect
+                               $status->warning( 'mergehistory-warning-redirect-not-created' );
+                       }
+               } else {
+                       $this->source->invalidateCache(); // update histories
+               }
+               $this->dest->invalidateCache(); // update histories
+
+               // Update our logs
+               $logEntry = new ManualLogEntry( 'merge', 'merge' );
+               $logEntry->setPerformer( $user );
+               $logEntry->setComment( $reason );
+               $logEntry->setTarget( $this->source );
+               $logEntry->setParameters( array(
+                       '4::dest' => $this->dest->getPrefixedText(),
+                       '5::mergepoint' => $this->timestampLimit->getTimestamp( TS_MW )
+               ) );
+               $logId = $logEntry->insert();
+               $logEntry->publish( $logId );
+
+               Hooks::run( 'ArticleMergeComplete', array( $this->source, $this->dest ) );
+
+               return $status;
+       }
+}
index 3adef5b..317c126 100644 (file)
@@ -1573,11 +1573,42 @@ class OutputPage extends ContextSource {
         * @return ParserOptions
         */
        public function parserOptions( $options = null ) {
+               if ( $options !== null && !empty( $options->isBogus ) ) {
+                       // Someone is trying to set a bogus pre-$wgUser PO. Check if it has
+                       // been changed somehow, and keep it if so.
+                       $anonPO = ParserOptions::newFromAnon();
+                       $anonPO->setEditSection( false );
+                       if ( !$options->matches( $anonPO ) ) {
+                               wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
+                               $options->isBogus = false;
+                       }
+               }
+
                if ( !$this->mParserOptions ) {
+                       if ( !$this->getContext()->getUser()->isSafeToLoad() ) {
+                               // $wgUser isn't unstubbable yet, so don't try to get a
+                               // ParserOptions for it. And don't cache this ParserOptions
+                               // either.
+                               $po = ParserOptions::newFromAnon();
+                               $po->setEditSection( false );
+                               $po->isBogus = true;
+                               if ( $options !== null ) {
+                                       $this->mParserOptions = empty( $options->isBogus ) ? $options : null;
+                               }
+                               return $po;
+                       }
+
                        $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
                        $this->mParserOptions->setEditSection( false );
                }
-               return wfSetVar( $this->mParserOptions, $options );
+
+               if ( $options !== null && !empty( $options->isBogus ) ) {
+                       // They're trying to restore the bogus pre-$wgUser PO. Do the right
+                       // thing.
+                       return wfSetVar( $this->mParserOptions, null, true );
+               } else {
+                       return wfSetVar( $this->mParserOptions, $options );
+               }
        }
 
        /**
index 643d1c4..9b70994 100644 (file)
@@ -43,8 +43,9 @@ class EditAction extends FormlessAction {
        public function show() {
                $this->useTransactionalTimeLimit();
 
+               $out = $this->getOutput();
+               $out->setRobotPolicy( 'noindex,nofollow' );
                if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
-                       $out = $this->getOutput();
                        $out->addModuleStyles( array(
                                'mediawiki.ui.input',
                                'mediawiki.ui.checkbox',
index 458fd18..f8192e5 100644 (file)
@@ -90,6 +90,7 @@ class ApiMain extends ApiBase {
                'revisiondelete' => 'ApiRevisionDelete',
                'managetags' => 'ApiManageTags',
                'tag' => 'ApiTag',
+               'mergehistory' => 'ApiMergeHistory',
        );
 
        /**
diff --git a/includes/api/ApiMergeHistory.php b/includes/api/ApiMergeHistory.php
new file mode 100644 (file)
index 0000000..8fa9d28
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ *
+ *
+ * Created on Dec 29, 2015
+ *
+ * Copyright © 2015 Geoffrey Mon <geofbot@gmail.com>
+ *
+ * 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
+ */
+
+/**
+ * API Module to merge page histories
+ * @ingroup API
+ */
+class ApiMergeHistory extends ApiBase {
+
+       public function execute() {
+               $this->useTransactionalTimeLimit();
+
+               $user = $this->getUser();
+               $params = $this->extractRequestParams();
+
+               $this->requireOnlyOneParameter( $params, 'from', 'fromid' );
+               $this->requireOnlyOneParameter( $params, 'to', 'toid' );
+
+               // Get page objects (nonexistant pages get caught in MergeHistory::isValidMerge())
+               if ( isset( $params['from'] ) ) {
+                       $fromTitle = Title::newFromText( $params['from'] );
+                       if ( !$fromTitle || $fromTitle->isExternal() ) {
+                               $this->dieUsageMsg( array( 'invalidtitle', $params['from'] ) );
+                       }
+               } elseif ( isset( $params['fromid'] ) ) {
+                       $fromTitle = Title::newFromID( $params['fromid'] );
+                       if ( !$fromTitle ) {
+                               $this->dieUsageMsg( array( 'nosuchpageid', $params['fromid'] ) );
+                       }
+               }
+
+               if ( isset( $params['to'] ) ) {
+                       $toTitle = Title::newFromText( $params['to'] );
+                       if ( !$toTitle || $toTitle->isExternal() ) {
+                               $this->dieUsageMsg( array( 'invalidtitle', $params['to'] ) );
+                       }
+               } elseif ( isset( $params['toid'] ) ) {
+                       $toTitle = Title::newFromID( $params['toid'] );
+                       if ( !$toTitle ) {
+                               $this->dieUsageMsg( array( 'nosuchpageid', $params['toid'] ) );
+                       }
+               }
+
+               $reason = $params['reason'];
+               $timestamp = $params['timestamp'];
+
+               // Merge!
+               $status = $this->merge( $fromTitle, $toTitle, $timestamp, $reason );
+               if ( !$status->isOK() ) {
+                       $this->dieStatus( $status );
+               }
+
+               $r = array(
+                       'from' => $fromTitle->getPrefixedText(),
+                       'to' => $toTitle->getPrefixedText(),
+                       'timestamp' => wfTimestamp( TS_ISO_8601, $params['timestamp'] ),
+                       'reason' => $params['reason']
+               );
+               $result = $this->getResult();
+
+               $result->addValue( null, $this->getModuleName(), $r );
+       }
+
+       /**
+        * @param Title $from
+        * @param Title $to
+        * @param string $timestamp
+        * @param string $reason
+        * @return Status
+        */
+       protected function merge( Title $from, Title $to, $timestamp, $reason ) {
+               $mh = new MergeHistory( $from, $to, $timestamp );
+
+               return $mh->merge( $this->getUser(), $reason );
+       }
+
+       public function mustBePosted() {
+               return true;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'from' => null,
+                       'fromid' => array(
+                               ApiBase::PARAM_TYPE => 'integer'
+                       ),
+                       'to' => null,
+                       'toid' => array(
+                               ApiBase::PARAM_TYPE => 'integer'
+                       ),
+                       'timestamp' => array(
+                               ApiBase::PARAM_TYPE => 'timestamp'
+                       ),
+                       'reason' => '',
+               );
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       protected function getExamplesMessages() {
+               return array(
+                       'action=mergehistory&from=Oldpage&to=Newpage&token=123ABC&' .
+                       'reason=Reason'
+                       => 'apihelp-mergehistory-example-merge',
+                       'action=mergehistory&from=Oldpage&to=Newpage&token=123ABC&' .
+                       'reason=Reason&timestamp=2015-12-31T04%3A37%3A41Z' // TODO
+                       => 'apihelp-mergehistory-example-merge-timestamp',
+               );
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Mergehistory';
+       }
+}
index a1b303f..4a1f2f1 100644 (file)
        "apihelp-managetags-example-activate": "Activate a tag named <kbd>spam</kbd> with the reason <kbd>For use in edit patrolling</kbd>",
        "apihelp-managetags-example-deactivate": "Deactivate a tag named <kbd>spam</kbd> with the reason <kbd>No longer required</kbd>",
 
+       "apihelp-mergehistory-description": "Merge page histories.",
+       "apihelp-mergehistory-param-from": "Title of the page from which history will be merged. Cannot be used together with <var>$1fromid</var>.",
+       "apihelp-mergehistory-param-fromid": "Page ID of the page from which history will be merged. Cannot be used together with <var>$1from</var>.",
+       "apihelp-mergehistory-param-to": "Title of the page to which history will be merged. Cannot be used together with <var>$1toid</var>.",
+       "apihelp-mergehistory-param-toid": "Page ID of the page to which history will be merged. Cannot be used together with <var>$1to</var>.",
+       "apihelp-mergehistory-param-timestamp": "Timestamp up to which revisions will be moved from the source page's history to the destination page's history. If omitted, the entire page history of the source page will be merged into the destination page.",
+       "apihelp-mergehistory-param-reason": "Reason for the history merge.",
+       "apihelp-mergehistory-example-merge": "Merge the entire history of <kbd>Oldpage</kbd> into <kbd>Newpage</kbd>.",
+       "apihelp-mergehistory-example-merge-timestamp": "Merge the page revisions of <kbd>Oldpage</kbd> dating up to <kbd>2015-12-31T04:37:41Z</kbd> into <kbd>Newpage</kbd>.",
+
        "apihelp-move-description": "Move a page.",
        "apihelp-move-param-from": "Title of the page to rename. Cannot be used together with <var>$1fromid</var>.",
        "apihelp-move-param-fromid": "Page ID of the page to rename. Cannot be used together with <var>$1from</var>.",
index e7bfbe1..8ac4d51 100644 (file)
        "api-help-datatypes-header": "Datentypen",
        "api-help-param-type-user": "Typ: {{PLURAL:$1|1=Benotzernumm|2=Lëscht vu Benotzernimm}}",
        "api-help-examples": "{{PLURAL:$1|Beispill|Beispiler}}:",
-       "api-help-permissions": "{{PLURAL:$1|Autorisatioun|Autorisatiounen}}:"
+       "api-help-permissions": "{{PLURAL:$1|Autorisatioun|Autorisatiounen}}:",
+       "api-help-open-in-apisandbox": "<small>[an der Sandkëscht opmaachen]</small>"
 }
index e3354aa..df4f881 100644 (file)
        "apihelp-managetags-example-delete": "{{doc-apihelp-example|managetags|info={{doc-important|The text \"vandlaism\" in this message is intentionally misspelled; the example being documented by this message is the deletion of a misspelled tag.}}}}",
        "apihelp-managetags-example-activate": "{{doc-apihelp-example|managetags}}",
        "apihelp-managetags-example-deactivate": "{{doc-apihelp-example|managetags}}",
+       "apihelp-mergehistory-description": "{{doc-apihelp-description|mergehistory}}",
+       "apihelp-mergehistory-param-from": "{{doc-apihelp-param|mergehistory|from}}",
+       "apihelp-mergehistory-param-fromid": "{{doc-apihelp-param|mergehistory|fromid}}",
+       "apihelp-mergehistory-param-to": "{{doc-apihelp-param|mergehistory|to}}",
+       "apihelp-mergehistory-param-toid": "{{doc-apihelp-param|mergehistory|toid}}",
+       "apihelp-mergehistory-param-timestamp": "{{doc-apihelp-param|mergehistory|timestamp}}",
+       "apihelp-mergehistory-param-reason": "{{doc-apihelp-param|mergehistory|reason}}",
+       "apihelp-mergehistory-example-merge": "{{doc-apihelp-example|mergehistory}}",
+       "apihelp-mergehistory-example-merge-timestamp": "{{doc-apihelp-example|mergehistory}}",
        "apihelp-move-description": "{{doc-apihelp-description|move}}",
        "apihelp-move-param-from": "{{doc-apihelp-param|move|from}}",
        "apihelp-move-param-fromid": "{{doc-apihelp-param|move|fromid}}",
index a2b2f73..3c667f1 100644 (file)
@@ -17,7 +17,8 @@
                        "RyRubyy",
                        "Umherirrender",
                        "Apflu",
-                       "Hzy980512"
+                       "Hzy980512",
+                       "PhiLiP"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|文档]]\n* [[mw:API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>本页所展示的所有特性都应正常工作,但是API仍在开发当中,将会随时变化。请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:API:Errors_and_warnings|API: 错误与警告]]。\n\n<strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。",
        "apihelp-query+allrevisions-param-generatetitles": "当作为生成器使用时,生成标题而不是修订ID。",
        "apihelp-query+allrevisions-example-user": "列出由用户<kbd>Example</kbd>作出的最近50次贡献。",
        "apihelp-query+allrevisions-example-ns-main": "列举主名字空间中的前50次修订。",
-       "apihelp-query+mystashedfiles-description": "获取当前用户的上传藏匿中的文件列表。",
+       "apihelp-query+mystashedfiles-description": "获取当前用户上传暂存库中的文件列表。",
        "apihelp-query+mystashedfiles-param-prop": "要检索文件的属性。",
        "apihelp-query+mystashedfiles-paramvalue-prop-size": "检索文件大小和图片尺寸。",
        "apihelp-query+mystashedfiles-paramvalue-prop-type": "检索文件的MIME类型和媒体类型。",
-       "apihelp-query+mystashedfiles-param-limit": "获取多少文件。",
+       "apihelp-query+mystashedfiles-param-limit": "要获取文件的数量。",
+       "apihelp-query+mystashedfiles-example-simple": "获取当前用户上传暂存库中的文件的filekey、大小和像素尺寸。",
        "apihelp-query+alltransclusions-description": "列出所有嵌入页面(使用&#123;&#123;x&#125;&#125;嵌入的页面),包括不存在的。",
        "apihelp-query+alltransclusions-param-from": "要列举的起始嵌入标题。",
        "apihelp-query+alltransclusions-param-to": "要列举的最终嵌入标题。",
        "apihelp-query+imageinfo-param-urlwidth": "如果$2prop=url被设定,将返回至缩放到此宽度的一张图片的URL。\n由于性能原因,如果此消息被使用,将不会返回超过$1张被缩放的图片。",
        "apihelp-query+imageinfo-param-urlheight": "与$1urlwidth类似。",
        "apihelp-query+imageinfo-param-metadataversion": "要使用的元数据版本。如果<kbd>latest</kbd>被指定,则使用最新版本。默认为<kbd>1</kbd>以便向下兼容。",
-       "apihelp-query+imageinfo-param-extmetadatalanguage": "要取得extmetadata的语言。This affects both which translation to fetch, if multiple are available, as well as how things like numbers and various values are formatted.",
+       "apihelp-query+imageinfo-param-extmetadatalanguage": "要取得extmetadata的语言。这会影响到抓取翻译的选择,如果有多个可用的话,还会影响到数字等数值的格式。",
        "apihelp-query+imageinfo-param-extmetadatamultilang": "如果用于extmetadata属性的翻译可用,则全部取得。",
        "apihelp-query+imageinfo-param-extmetadatafilter": "如果指定且非空,则只为$1prop=extmetadata返回这些键。",
        "apihelp-query+imageinfo-param-urlparam": "处理器特定的参数字符串。例如PDF可能使用<kbd>page15-100px</kbd>。<var>$1urlwidth</var>必须被使用,并与<var>$1urlparam</var>一致。",
        "apihelp-query+redirects-param-show": "只显示符合这些标准的项目:\n;fragment:只显示带碎片的重定向。\n;!fragment:只显示不带碎片的重定向。",
        "apihelp-query+redirects-example-simple": "获取至[[Main Page]]的重定向列表。",
        "apihelp-query+redirects-example-generator": "获取所有重定向至[[Main Page]]的信息。",
-       "apihelp-query+revisions-description": "获取修订版本信息。\n\n可用于以下几个方面:\n# Get data about a set of pages (last revision), by setting titles or pageids.\n# Get revisions for one given page, by using titles or pageids with start, end, or limit.\n# Get data about a set of revisions by setting their IDs with revids.",
+       "apihelp-query+revisions-description": "获取修订版本信息。\n\n可用于以下几个方面:\n# 通过设置title或pageid获取一批页面(最新修订)的数据。\n# 通过使用带start、end或limit的title或pageid获取给定页面的多个修订。\n# 通过revid设置一批修订的ID获取它们的数据。",
        "apihelp-query+revisions-paraminfo-singlepageonly": "可能只能与单一页面使用(模式#2)。",
        "apihelp-query+revisions-param-startid": "从哪个修订版本ID开始列举。",
        "apihelp-query+revisions-param-endid": "在此修订版本ID停止修订列举。",
        "apihelp-query+usercontribs-param-start": "返回的起始时间戳。",
        "apihelp-query+usercontribs-param-end": "返回的最终时间戳。",
        "apihelp-query+usercontribs-param-user": "要检索贡献的用户。",
+       "apihelp-query+usercontribs-param-userprefix": "取得所有用户名以这个值开头的用户的贡献。覆盖$1user。",
        "apihelp-query+usercontribs-param-namespace": "只列出这些名字空间的贡献。",
        "apihelp-query+usercontribs-param-prop": "包含额外的信息束:",
        "apihelp-query+usercontribs-paramvalue-prop-ids": "添加页面ID和修订ID。",
        "apihelp-watch-example-unwatch": "取消监视页面<kbd>Main Page</kbd>。",
        "apihelp-watch-example-generator": "监视主名字空间中的最少几个页面。",
        "apihelp-format-example-generic": "返回查询结果为$1格式。",
+       "apihelp-format-param-wrappedhtml": "作为一个JSON对象返回渲染好的HTML和关联的ResouceLoader模块。",
        "apihelp-json-description": "输出数据为JSON格式。",
        "apihelp-json-param-callback": "如果指定,将输出内容包裹在一个指定的函数调用中。出于安全考虑,所有用户相关的数据将被限制。",
        "apihelp-json-param-utf8": "如果指定,使用十六进制转义序列将大多数(但不是全部)非ASCII的字符编码为UTF-8,而不是替换它们。默认当<var>formatversion</var>不是<kbd>1</kbd>时。",
index de84199..e61e2d2 100644 (file)
@@ -223,6 +223,7 @@ abstract class Installer {
                // $wgLogo is probably wrong (bug 48084); set something that will work.
                // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
                'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png',
+               'wgAuthenticationTokenVersion' => 1,
        );
 
        /**
index 3b6a37f..4f20c70 100644 (file)
@@ -65,7 +65,7 @@ class LocalSettingsGenerator {
                                'wgRightsText', '_MainCacheType', 'wgEnableUploads',
                                '_MemCachedServers', 'wgDBserver', 'wgDBuser',
                                'wgDBpassword', 'wgUseInstantCommons', 'wgUpgradeKey', 'wgDefaultSkin',
-                               'wgMetaNamespace', 'wgLogo',
+                               'wgMetaNamespace', 'wgLogo', 'wgAuthenticationTokenVersion',
                        ),
                        $db->getGlobalNames()
                );
@@ -396,6 +396,9 @@ ${serverSetting}
 
 \$wgSecretKey = \"{$this->values['wgSecretKey']}\";
 
+# Changing this will log out all existing sessions.
+\$wgAuthenticationTokenVersion = \"{$this->values['wgAuthenticationTokenVersion']}\";
+
 # Site upgrade key. Must be set to a string (default provided) to turn on the
 # web installer while LocalSettings.php is in place
 \$wgUpgradeKey = \"{$this->values['wgUpgradeKey']}\";
index 2c08c9c..1d17c94 100644 (file)
@@ -178,6 +178,13 @@ class WebInstallerExistingWiki extends WebInstallerPage {
                // All good
                $this->setVar( '_ExistingDBSettings', true );
 
+               // Copy $wgAuthenticationTokenVersion too, if it exists
+               $this->setVar( 'wgAuthenticationTokenVersion',
+                       isset( $vars['wgAuthenticationTokenVersion'] )
+                               ? $vars['wgAuthenticationTokenVersion']
+                               : null
+               );
+
                return $status;
        }
 
index b7eef8f..598d956 100644 (file)
@@ -1793,6 +1793,8 @@ class WikiPage implements Page, IDBAccessObject {
 
                $changed = !$content->equals( $oldContent );
 
+               $dbw = wfGetDB( DB_MASTER );
+
                if ( $changed ) {
                        $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                        $status->merge( $prepStatus );
@@ -1800,14 +1802,13 @@ class WikiPage implements Page, IDBAccessObject {
                                return $status;
                        }
 
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->begin( __METHOD__ );
+                       $dbw->startAtomic( __METHOD__ );
                        // Get the latest page_latest value while locking it.
                        // Do a CAS style check to see if it's the same as when this method
                        // started. If it changed then bail out before touching the DB.
                        $latestNow = $this->lockAndGetLatest();
                        if ( $latestNow != $oldid ) {
-                               $dbw->commit( __METHOD__ );
+                               $dbw->endAtomic( __METHOD__ );
                                // Page updated or deleted in the mean time
                                $status->fatal( 'edit-conflict' );
 
@@ -1855,7 +1856,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                        $user->incEditCount();
 
-                       $dbw->commit( __METHOD__ );
+                       $dbw->endAtomic( __METHOD__ );
                        $this->mTimestamp = $now;
                } else {
                        // Bug 32948: revision ID must be set to page {{REVISIONID}} and
@@ -1863,17 +1864,6 @@ class WikiPage implements Page, IDBAccessObject {
                        $revision->setId( $this->getLatest() );
                }
 
-               // Update links tables, site stats, etc.
-               $this->doEditUpdates(
-                       $revision,
-                       $user,
-                       array(
-                               'changed' => $changed,
-                               'oldcountable' => $meta['oldCountable'],
-                               'oldrevision' => $meta['oldRevision']
-                       )
-               );
-
                if ( $changed ) {
                        // Return the new revision to the caller
                        $status->value['revision'] = $revision;
@@ -1884,11 +1874,32 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mTitle->invalidateCache( $now );
                }
 
-               // Trigger post-save hook
-               $hook_args = array( &$this, &$user, $content, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $meta['baseRevId'] );
-               ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
-               Hooks::run( 'PageContentSaveComplete', $hook_args );
+               // Do secondary updates once the main changes have been committed...
+               $that = $this;
+               $dbw->onTransactionIdle(
+                       function () use (
+                               $dbw, &$that, $revision, &$user, $content, $summary, &$flags,
+                               $changed, $meta, &$status
+                       ) {
+                               // Do per-page updates in a transaction
+                               $dbw->setFlag( DBO_TRX );
+                               // Update links tables, site stats, etc.
+                               $that->doEditUpdates(
+                                       $revision,
+                                       $user,
+                                       array(
+                                               'changed' => $changed,
+                                               'oldcountable' => $meta['oldCountable'],
+                                               'oldrevision' => $meta['oldRevision']
+                                       )
+                               );
+                               // Trigger post-save hook
+                               $params = array( &$that, &$user, $content, $summary, $flags & EDIT_MINOR,
+                                       null, null, &$flags, $revision, &$status, $meta['baseRevId'] );
+                               ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
+                               Hooks::run( 'PageContentSaveComplete', $params );
+                       }
+               );
 
                return $status;
        }
index 6329fd7..7fc9a16 100644 (file)
@@ -440,8 +440,11 @@ class LinkHolderArray {
                # Make interwiki link HTML
                $output = $this->parent->getOutput();
                $replacePairs = array();
+               $options = array(
+                       'stubThreshold' => $this->parent->getOptions()->getStubThreshold(),
+               );
                foreach ( $this->interwikis as $key => $link ) {
-                       $replacePairs[$key] = Linker::link( $link['title'], $link['text'] );
+                       $replacePairs[$key] = Linker::link( $link['title'], $link['text'], array(), array(), $options );
                        $output->addInterwikiLink( $link['title'] );
                }
                $replacer = new HashtableReplacer( $replacePairs, 1 );
index d9c60c7..81c7ebf 100644 (file)
@@ -118,26 +118,44 @@ class BotPasswordSessionProvider extends ImmutableSessionProviderWithCookie {
                        array_keys( $metadata )
                );
                if ( $missingKeys ) {
-                       $this->logger->info( "Session $info: Missing metadata: " . join( ', ', $missingKeys ) );
+                       $this->logger->info( 'Session "{session}": Missing metadata: {missing}', array(
+                               'session' => $info,
+                               'missing' => join( ', ', $missingKeys ),
+                       ) );
                        return false;
                }
 
                $bp = BotPassword::newFromCentralId( $metadata['centralId'], $metadata['appId'] );
                if ( !$bp ) {
                        $this->logger->info(
-                               "Session $info: No BotPassword for {$metadata['centralId']} {$metadata['appId']}"
-                       );
+                               'Session "{session}": No BotPassword for {centralId} {appId}',
+                               array(
+                                       'session' => $info,
+                                       'centralId' => $metadata['centralId'],
+                                       'appId' => $metadata['appId'],
+                       ) );
                        return false;
                }
 
                if ( !hash_equals( $metadata['token'], $bp->getToken() ) ) {
-                       $this->logger->info( "Session $info: BotPassword token check failed" );
+                       $this->logger->info( 'Session "{session}": BotPassword token check failed', array(
+                               'session' => $info,
+                               'centralId' => $metadata['centralId'],
+                               'appId' => $metadata['appId'],
+                       ) );
                        return false;
                }
 
                $status = $bp->getRestrictions()->check( $request );
                if ( !$status->isOk() ) {
-                       $this->logger->info( "Session $info: Restrictions check failed", $status->getValue() );
+                       $this->logger->info(
+                               'Session "{session}": Restrictions check failed',
+                               array(
+                                       'session' => $info,
+                                       'restrictions' => $status->getValue(),
+                                       'centralId' => $metadata['centralId'],
+                                       'appId' => $metadata['appId'],
+                       ) );
                        return false;
                }
 
index f989cbc..3177dc2 100644 (file)
@@ -123,11 +123,28 @@ class CookieSessionProvider extends SessionProvider {
 
                        // Sanity check
                        if ( $userName !== null && $userInfo->getName() !== $userName ) {
+                               $this->logger->warning(
+                                       'Session "{session}" requested with mismatched UserID and UserName cookies.',
+                                       array(
+                                               'session' => $info['id'],
+                                               'mismatch' => array(
+                                                       'userid' => $userId,
+                                                       'cookie_username' => $userName,
+                                                       'username' => $userInfo->getName(),
+                                               ),
+                               ) );
                                return null;
                        }
 
                        if ( $token !== null ) {
                                if ( !hash_equals( $userInfo->getToken(), $token ) ) {
+                                       $this->logger->warning(
+                                               'Session "{session}" requested with invalid Token cookie.',
+                                               array(
+                                                       'session' => $info['id'],
+                                                       'userid' => $userId,
+                                                       'username' => $userInfo->getName(),
+                                        ) );
                                        return null;
                                }
                                $info['userInfo'] = $userInfo->verified();
@@ -140,6 +157,15 @@ class CookieSessionProvider extends SessionProvider {
                        }
                } elseif ( isset( $info['id'] ) ) {
                        // No UserID cookie, so insist that the session is anonymous.
+                       // Note: this event occurs for several normal activities:
+                       // * anon visits Special:UserLogin
+                       // * anon browsing after seeing Special:UserLogin
+                       // * anon browsing after edit or preview
+                       $this->logger->debug(
+                               'Session "{session}" requested without UserID cookie',
+                               array(
+                                       'session' => $info['id'],
+                       ) );
                        $info['userInfo'] = UserInfo::newAnonymous();
                } else {
                        // No session ID and no user is the same as an empty session, so
index 4dea274..795e253 100644 (file)
@@ -247,8 +247,10 @@ class PHPSessionHandler {
                        // This can happen under normal circumstances, if the session exists but is
                        // invalid. Let's emit a log warning instead of a PHP warning.
                        $this->logger->warning(
-                               __METHOD__ . ": Session \"$id\" cannot be loaded, skipping write."
-                       );
+                               __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
+                               array(
+                                       'session' => $id,
+                       ) );
                        return true;
                }
 
index 2bff173..a79c5cb 100644 (file)
@@ -138,7 +138,11 @@ final class SessionBackend {
                        $this->data = array();
                        $this->dataDirty = true;
                        $this->metaDirty = true;
-                       $this->logger->debug( "SessionBackend $this->id is unsaved, marking dirty in constructor" );
+                       $this->logger->debug(
+                               'SessionBackend "{session}" is unsaved, marking dirty in constructor',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                } else {
                        $this->data = $blob['data'];
                        if ( isset( $blob['metadata']['loggedOut'] ) ) {
@@ -149,8 +153,10 @@ final class SessionBackend {
                        } else {
                                $this->metaDirty = true;
                                $this->logger->debug(
-                                       "SessionBackend $this->id metadata dirty due to missing expiration timestamp"
-                               );
+                                       'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
+                               array(
+                                       'session' => $this->id,
+                               ) );
                        }
                }
                $this->dataHash = md5( serialize( $this->data ) );
@@ -218,8 +224,11 @@ final class SessionBackend {
                        $this->provider->sessionIdWasReset( $this, $oldId );
                        $this->metaDirty = true;
                        $this->logger->debug(
-                               "SessionBackend $this->id metadata dirty due to ID reset (formerly $oldId)"
-                       );
+                               'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
+                               array(
+                                       'session' => $this->id,
+                                       'oldId' => $oldId,
+                       ) );
 
                        if ( $restart ) {
                                session_id( (string)$this->id );
@@ -263,7 +272,11 @@ final class SessionBackend {
                        $this->persist = true;
                        $this->forcePersist = true;
                        $this->metaDirty = true;
-                       $this->logger->debug( "SessionBackend $this->id force-persist due to persist()" );
+                       $this->logger->debug(
+                               'SessionBackend "{session}" force-persist due to persist()',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                        $this->autosave();
                } else {
                        $this->renew();
@@ -288,7 +301,11 @@ final class SessionBackend {
                if ( $this->remember !== (bool)$remember ) {
                        $this->remember = (bool)$remember;
                        $this->metaDirty = true;
-                       $this->logger->debug( "SessionBackend $this->id metadata dirty due to remember-user change" );
+                       $this->logger->debug(
+                               'SessionBackend "{session}" metadata dirty due to remember-user change',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                        $this->autosave();
                }
        }
@@ -345,7 +362,11 @@ final class SessionBackend {
 
                $this->user = $user;
                $this->metaDirty = true;
-               $this->logger->debug( "SessionBackend $this->id metadata dirty due to user change" );
+               $this->logger->debug(
+                       'SessionBackend "{session}" metadata dirty due to user change',
+                       array(
+                               'session' => $this->id,
+               ) );
                $this->autosave();
        }
 
@@ -377,7 +398,11 @@ final class SessionBackend {
                if ( $this->forceHTTPS !== (bool)$force ) {
                        $this->forceHTTPS = (bool)$force;
                        $this->metaDirty = true;
-                       $this->logger->debug( "SessionBackend $this->id metadata dirty due to force-HTTPS change" );
+                       $this->logger->debug(
+                               'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                        $this->autosave();
                }
        }
@@ -400,8 +425,10 @@ final class SessionBackend {
                        $this->loggedOut = $ts;
                        $this->metaDirty = true;
                        $this->logger->debug(
-                               "SessionBackend $this->id metadata dirty due to logged-out-timestamp change"
-                       );
+                               'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                        $this->autosave();
                }
        }
@@ -428,8 +455,10 @@ final class SessionBackend {
                        $this->providerMetadata = $metadata;
                        $this->metaDirty = true;
                        $this->logger->debug(
-                               "SessionBackend $this->id metadata dirty due to provider metadata change"
-                       );
+                               'SessionBackend "{session}" metadata dirty due to provider metadata change',
+                               array(
+                                       'session' => $this->id,
+                       ) );
                        $this->autosave();
                }
        }
@@ -461,8 +490,11 @@ final class SessionBackend {
                                $data[$key] = $value;
                                $this->dataDirty = true;
                                $this->logger->debug(
-                                       "SessionBackend $this->id data dirty due to addData(): " . wfGetAllCallers( 5 )
-                               );
+                                       'SessionBackend "{session}" data dirty due to addData(): {callers}',
+                                       array(
+                                               'session' => $this->id,
+                                               'callers' => wfGetAllCallers( 5 ),
+                               ) );
                        }
                }
        }
@@ -474,8 +506,11 @@ final class SessionBackend {
        public function dirty() {
                $this->dataDirty = true;
                $this->logger->debug(
-                       "SessionBackend $this->id data dirty due to dirty(): " . wfGetAllCallers( 5 )
-               );
+                       'SessionBackend "{session}" data dirty due to dirty(): {callers}',
+                       array(
+                               'session' => $this->id,
+                               'callers' => wfGetAllCallers( 5 ),
+               ) );
        }
 
        /**
@@ -488,13 +523,19 @@ final class SessionBackend {
                if ( time() + $this->lifetime / 2 > $this->expires ) {
                        $this->metaDirty = true;
                        $this->logger->debug(
-                               "SessionBackend $this->id metadata dirty for renew(): " . wfGetAllCallers( 5 )
-                       );
+                               'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
+                               array(
+                                       'session' => $this->id,
+                                       'callers' => wfGetAllCallers( 5 ),
+                       ) );
                        if ( $this->persist ) {
                                $this->forcePersist = true;
                                $this->logger->debug(
-                                       "SessionBackend $this->id force-persist for renew(): " . wfGetAllCallers( 5 )
-                               );
+                                       'SessionBackend "{session}" force-persist for renew(): {callers}',
+                                       array(
+                                               'session' => $this->id,
+                                               'callers' => wfGetAllCallers( 5 ),
+                               ) );
                        }
                }
                $this->autosave();
@@ -535,9 +576,12 @@ final class SessionBackend {
        public function save( $closing = false ) {
                if ( $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
                        $this->logger->debug(
-                               "SessionBackend $this->id not saving, " .
-                                       "user {$this->user} was passed to SessionManager::preventSessionsForUser"
-                       );
+                               'SessionBackend "{session}" not saving, user {user} was ' .
+                               'passed to SessionManager::preventSessionsForUser',
+                               array(
+                                       'session' => $this->id,
+                                       'user' => $this->user,
+                       ) );
                        return;
                }
 
@@ -546,8 +590,11 @@ final class SessionBackend {
                $anon = $this->user->isAnon();
                if ( !$anon && !$this->user->getToken( false ) ) {
                        $this->logger->debug(
-                               "SessionBackend $this->id creating token for user {$this->user} on save"
-                       );
+                               'SessionBackend "{session}" creating token for user {user} on save',
+                               array(
+                                       'session' => $this->id,
+                                       'user' => $this->user,
+                       ) );
                        $this->user->setToken();
                        if ( !wfReadOnly() ) {
                                $this->user->saveSettings();
@@ -559,8 +606,13 @@ final class SessionBackend {
                if ( !$this->metaDirty && !$this->dataDirty &&
                        $this->dataHash !== md5( serialize( $this->data ) )
                ) {
-                       $this->logger->debug( "SessionBackend $this->id data dirty due to hash mismatch, " .
-                               "$this->dataHash !== " . md5( serialize( $this->data ) ) );
+                       $this->logger->debug(
+                               'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
+                               array(
+                                       'session' => $this->id,
+                                       'expected' => $this->dataHash,
+                                       'got' => md5( serialize( $this->data ) ),
+                       ) );
                        $this->dataDirty = true;
                }
 
@@ -568,11 +620,15 @@ final class SessionBackend {
                        return;
                }
 
-               $this->logger->debug( "SessionBackend $this->id save: " .
-                       'dataDirty=' . (int)$this->dataDirty . ' ' .
-                       'metaDirty=' . (int)$this->metaDirty . ' ' .
-                       'forcePersist=' . (int)$this->forcePersist
-               );
+               $this->logger->debug(
+                       'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
+                       'metaDirty={metaDirty} forcePersist={forcePersist}',
+                       array(
+                               'session' => $this->id,
+                               'dataDirty' => (int)$this->dataDirty,
+                               'metaDirty' => (int)$this->metaDirty,
+                               'forcePersist' => (int)$this->forcePersist,
+               ) );
 
                // Persist to the provider, if flagged
                if ( $this->persist && ( $this->metaDirty || $this->forcePersist ) ) {
@@ -644,7 +700,11 @@ final class SessionBackend {
                        if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
                                SessionManager::getGlobalSession()->getId() === (string)$this->id
                        ) {
-                               $this->logger->debug( "SessionBackend $this->id: Taking over PHP session" );
+                               $this->logger->debug(
+                                       'SessionBackend "{session}" Taking over PHP session',
+                                       array(
+                                               'session' => $this->id,
+                               ) );
                                session_id( (string)$this->id );
                                \MediaWiki\quietCall( 'session_start' );
                        }
index f03260f..07291e9 100644 (file)
@@ -216,8 +216,11 @@ final class SessionManager implements SessionManagerInterface {
                        try {
                                $session = $this->getEmptySessionInternal( $request, $id );
                        } catch ( \Exception $ex ) {
-                               $this->logger->error( __METHOD__ . ': failed to create empty session: ' .
-                                       $ex->getMessage() );
+                               $this->logger->error( 'Failed to create empty session: {exception}',
+                                       array(
+                                               'method' => __METHOD__,
+                                               'exception' => $ex,
+                               ) );
                                $session = null;
                        }
                }
@@ -462,14 +465,21 @@ final class SessionManager implements SessionManagerInterface {
 
                // Checks passed, create the user...
                $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
-               $logger->info( __METHOD__ . ": creating new user ($userName) - from: $from" );
+               $logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}',
+                       array(
+                               'username' => $userName,
+                               'url' => $from,
+               ) );
 
                try {
                        // Insert the user into the local DB master
                        $status = $user->addToDatabase();
                        if ( !$status->isOK() ) {
                                // @codeCoverageIgnoreStart
-                               $logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText() );
+                               $logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText(),
+                                       array(
+                                               'username' => $userName,
+                               ) );
                                $user->setId( 0 );
                                $user->loadFromId();
                                return false;
@@ -477,7 +487,10 @@ final class SessionManager implements SessionManagerInterface {
                        }
                } catch ( \Exception $ex ) {
                        // @codeCoverageIgnoreStart
-                       $logger->error( __METHOD__ . ': failed with exception ' . $ex->getMessage() );
+                       $logger->error( __METHOD__ . ': failed with exception {exception}', array(
+                               'exception' => $ex,
+                               'username' => $userName,
+                       ) );
                        // Do not keep throwing errors for a while
                        $cache->set( $backoffKey, 1, 600 );
                        // Bubble up error; which should normally trigger DB rollbacks
@@ -662,7 +675,9 @@ final class SessionManager implements SessionManagerInterface {
                if ( $blob !== false ) {
                        // Sanity check: blob must be an array, if it's saved at all
                        if ( !is_array( $blob ) ) {
-                               $this->logger->warning( "Session $info: Bad data" );
+                               $this->logger->warning( 'Session "{session}": Bad data', array(
+                                       'session' => $info,
+                               ) );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -671,7 +686,9 @@ final class SessionManager implements SessionManagerInterface {
                        if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
                                !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
                        ) {
-                               $this->logger->warning( "Session $info: Bad data structure" );
+                               $this->logger->warning( 'Session "{session}": Bad data structure', array(
+                                       'session' => $info,
+                               ) );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -686,7 +703,9 @@ final class SessionManager implements SessionManagerInterface {
                                !array_key_exists( 'userToken', $metadata ) ||
                                !array_key_exists( 'provider', $metadata )
                        ) {
-                               $this->logger->warning( "Session $info: Bad metadata" );
+                               $this->logger->warning( 'Session "{session}": Bad metadata', array(
+                                       'session' => $info,
+                               ) );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -696,13 +715,21 @@ final class SessionManager implements SessionManagerInterface {
                        if ( $provider === null ) {
                                $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
                                if ( !$provider ) {
-                                       $this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
+                                       $this->logger->warning(
+                                               'Session "{session}": Unknown provider ' . $metadata['provider'],
+                                               array(
+                                                       'session' => $info,
+                                               )
+                                       );
                                        $this->store->delete( $key );
                                        return false;
                                }
                        } elseif ( $metadata['provider'] !== (string)$provider ) {
-                               $this->logger->warning( "Session $info: Wrong provider, " .
-                                       $metadata['provider'] . ' !== ' . $provider );
+                               $this->logger->warning( 'Session "{session}": Wrong provider ' .
+                                       $metadata['provider'] . ' !== ' . $provider,
+                                       array(
+                                               'session' => $info,
+                               ) );
                                return false;
                        }
 
@@ -720,7 +747,12 @@ final class SessionManager implements SessionManagerInterface {
                                                        $newParams['metadata'] = $newProviderMetadata;
                                                }
                                        } catch ( \UnexpectedValueException $ex ) {
-                                               $this->logger->warning( "Session $info: Metadata merge failed: " . $ex->getMessage() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": Metadata merge failed: {exception}',
+                                                       array(
+                                                               'session' => $info,
+                                                               'exception' => $ex,
+                                               ) );
                                                return false;
                                        }
                                }
@@ -739,7 +771,10 @@ final class SessionManager implements SessionManagerInterface {
                                                $userInfo = UserInfo::newAnonymous();
                                        }
                                } catch ( \InvalidArgumentException $ex ) {
-                                       $this->logger->error( "Session $info: " . $ex->getMessage() );
+                                       $this->logger->error( 'Session "{session}": {exception}', array(
+                                               'session' => $info,
+                                               'exception' => $ex,
+                                       ) );
                                        return false;
                                }
                                $newParams['userInfo'] = $userInfo;
@@ -748,8 +783,13 @@ final class SessionManager implements SessionManagerInterface {
                                // is no saved ID and the names match.
                                if ( $metadata['userId'] ) {
                                        if ( $metadata['userId'] !== $userInfo->getId() ) {
-                                               $this->logger->warning( "Session $info: User ID mismatch, " .
-                                                       $metadata['userId'] . ' !== ' . $userInfo->getId() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
+                                                       array(
+                                                               'session' => $info,
+                                                               'uid_a' => $metadata['userId'],
+                                                               'uid_b' => $userInfo->getId(),
+                                               ) );
                                                return false;
                                        }
 
@@ -757,24 +797,35 @@ final class SessionManager implements SessionManagerInterface {
                                        if ( $metadata['userName'] !== null &&
                                                $userInfo->getName() !== $metadata['userName']
                                        ) {
-                                               $this->logger->warning( "Session $info: User ID matched but name didn't (rename?), " .
-                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
+                                                       array(
+                                                               'session' => $info,
+                                                               'uname_a' => $metadata['userName'],
+                                                               'uname_b' => $userInfo->getName(),
+                                               ) );
                                                return false;
                                        }
 
                                } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
                                        if ( $metadata['userName'] !== $userInfo->getName() ) {
-                                               $this->logger->warning( "Session $info: User name mismatch, " .
-                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
+                                                       array(
+                                                               'session' => $info,
+                                                               'uname_a' => $metadata['userName'],
+                                                               'uname_b' => $userInfo->getName(),
+                                               ) );
                                                return false;
                                        }
                                } elseif ( !$userInfo->isAnon() ) {
                                        // Metadata specifies an anonymous user, but the passed-in
                                        // user isn't anonymous.
                                        $this->logger->warning(
-                                               "Session $info: Metadata has an anonymous user, " .
-                                                       'but a non-anon user was provided'
-                                       );
+                                               'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
+                                               array(
+                                                       'session' => $info,
+                                       ) );
                                        return false;
                                }
                        }
@@ -783,7 +834,9 @@ final class SessionManager implements SessionManagerInterface {
                        if ( $metadata['userToken'] !== null &&
                                $userInfo->getToken() !== $metadata['userToken']
                        ) {
-                               $this->logger->warning( "Session $info: User token mismatch" );
+                               $this->logger->warning( 'Session "{session}": User token mismatch', array(
+                                       'session' => $info,
+                               ) );
                                return false;
                        }
                        if ( !$userInfo->isVerified() ) {
@@ -806,7 +859,11 @@ final class SessionManager implements SessionManagerInterface {
                } else {
                        // No metadata, so we can't load the provider if one wasn't given.
                        if ( $info->getProvider() === null ) {
-                               $this->logger->warning( "Session $info: Null provider and no metadata" );
+                               $this->logger->warning(
+                                       'Session "{session}": Null provider and no metadata',
+                                       array(
+                                               'session' => $info,
+                               ) );
                                return false;
                        }
 
@@ -816,14 +873,18 @@ final class SessionManager implements SessionManagerInterface {
                                        $newParams['userInfo'] = UserInfo::newAnonymous();
                                } else {
                                        $this->logger->info(
-                                               "Session $info: No user provided and provider cannot set user"
-                                       );
+                                               'Session "{session}": No user provided and provider cannot set user',
+                                               array(
+                                                       'session' => $info,
+                                       ) );
                                        return false;
                                }
                        } elseif ( !$info->getUserInfo()->isVerified() ) {
                                $this->logger->warning(
-                                       "Session $info: Unverified user provided and no metadata to auth it"
-                               );
+                                       'Session "{session}": Unverified user provided and no metadata to auth it',
+                                       array(
+                                               'session' => $info,
+                               ) );
                                return false;
                        }
 
@@ -863,7 +924,9 @@ final class SessionManager implements SessionManagerInterface {
                        'SessionCheckInfo',
                        array( &$reason, $info, $request, $metadata, $data )
                ) ) {
-                       $this->logger->warning( "Session $info: $reason" );
+                       $this->logger->warning( 'Session "{session}": ' . $reason, array(
+                               'session' => $info,
+                       ) );
                        return false;
                }
 
index 28ff1c7..a164c1e 100644 (file)
@@ -262,15 +262,15 @@ class SpecialLog extends SpecialPage {
                // Select: All, None, Invert
                $links = array();
                $links[] = Html::element(
-                       'a', array( 'href' => '#', 'id' => 'checkbox-all' ),
+                       'a', array( 'href' => '#', 'class' => 'mw-checkbox-all' ),
                        $this->msg( 'checkbox-all' )->text()
                );
                $links[] = Html::element(
-                       'a', array( 'href' => '#', 'id' => 'checkbox-none' ),
+                       'a', array( 'href' => '#', 'class' => 'mw-checkbox-none' ),
                        $this->msg( 'checkbox-none' )->text()
                );
                $links[] = Html::element(
-                       'a', array( 'href' => '#', 'id' => 'checkbox-invert' ),
+                       'a', array( 'href' => '#', 'class' => 'mw-checkbox-invert' ),
                        $this->msg( 'checkbox-invert' )->text()
                );
 
index 0a25180..0cefb38 100644 (file)
@@ -347,138 +347,17 @@ class SpecialMergeHistory extends SpecialPage {
                if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
                        return false;
                }
-               # Verify that this timestamp is valid
-               # Must be older than the destination page
-               $dbw = wfGetDB( DB_MASTER );
-               # Get timestamp into DB format
-               $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp( $this->mTimestamp ) : '';
-               # Max timestamp should be min of destination page
-               $maxtimestamp = $dbw->selectField(
-                       'revision',
-                       'MIN(rev_timestamp)',
-                       array( 'rev_page' => $this->mDestID ),
-                       __METHOD__
-               );
-               # Destination page must exist with revisions
-               if ( !$maxtimestamp ) {
-                       $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
 
-                       return false;
-               }
-               # Get the latest timestamp of the source
-               $lasttimestamp = $dbw->selectField(
-                       array( 'page', 'revision' ),
-                       'rev_timestamp',
-                       array( 'page_id' => $this->mTargetID, 'page_latest = rev_id' ),
-                       __METHOD__
-               );
-               # $this->mTimestamp must be older than $maxtimestamp
-               if ( $this->mTimestamp >= $maxtimestamp ) {
-                       $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
+               // MergeHistory object
+               $mh = new MergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
 
+               // Merge!
+               $mergeStatus = $mh->merge( $this->getUser(), $this->mComment );
+               if ( !$mergeStatus->isOK() ) {
+                       // Failed merge
+                       $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
                        return false;
                }
-               # Get the timestamp pivot condition
-               if ( $this->mTimestamp ) {
-                       $timewhere = "rev_timestamp <= {$this->mTimestamp}";
-                       $timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp );
-               } else {
-                       $timewhere = "rev_timestamp <= {$maxtimestamp}";
-                       $timestampLimit = wfTimestamp( TS_MW, $lasttimestamp );
-               }
-               # Check that there are not too many revisions to move
-               $limit = 5000; // avoid too much slave lag
-               $count = $dbw->selectRowCount( 'revision', '1',
-                       array( 'rev_page' => $this->mTargetID, $timewhere ),
-                       __METHOD__,
-                       array( 'LIMIT' => $limit + 1 )
-               );
-               if ( $count > $limit ) {
-                       $this->getOutput()->addWikiMsg( 'mergehistory-fail-toobig' );
-
-                       return false;
-               }
-               # Do the moving...
-               $dbw->update(
-                       'revision',
-                       array( 'rev_page' => $this->mDestID ),
-                       array( 'rev_page' => $this->mTargetID, $timewhere ),
-                       __METHOD__
-               );
-
-               $count = $dbw->affectedRows();
-               # Make the source page a redirect if no revisions are left
-               $haveRevisions = $dbw->selectField(
-                       'revision',
-                       'rev_timestamp',
-                       array( 'rev_page' => $this->mTargetID ),
-                       __METHOD__,
-                       array( 'FOR UPDATE' )
-               );
-               if ( !$haveRevisions ) {
-                       if ( $this->mComment ) {
-                               $comment = $this->msg(
-                                       'mergehistory-comment',
-                                       $targetTitle->getPrefixedText(),
-                                       $destTitle->getPrefixedText(),
-                                       $this->mComment
-                               )->inContentLanguage()->text();
-                       } else {
-                               $comment = $this->msg(
-                                       'mergehistory-autocomment',
-                                       $targetTitle->getPrefixedText(),
-                                       $destTitle->getPrefixedText()
-                               )->inContentLanguage()->text();
-                       }
-
-                       $contentHandler = ContentHandler::getForTitle( $targetTitle );
-                       $redirectContent = $contentHandler->makeRedirectContent( $destTitle );
-
-                       if ( $redirectContent ) {
-                               $redirectPage = WikiPage::factory( $targetTitle );
-                               $redirectRevision = new Revision( array(
-                                       'title' => $targetTitle,
-                                       'page' => $this->mTargetID,
-                                       'comment' => $comment,
-                                       'content' => $redirectContent ) );
-                               $redirectRevision->insertOn( $dbw );
-                               $redirectPage->updateRevisionOn( $dbw, $redirectRevision );
-
-                               # Now, we record the link from the redirect to the new title.
-                               # It should have no other outgoing links...
-                               $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
-                               $dbw->insert( 'pagelinks',
-                                       array(
-                                               'pl_from' => $this->mDestID,
-                                               'pl_from_namespace' => $destTitle->getNamespace(),
-                                               'pl_namespace' => $destTitle->getNamespace(),
-                                               'pl_title' => $destTitle->getDBkey() ),
-                                       __METHOD__
-                               );
-                       } else {
-                               // would be nice to show a warning if we couldn't create a redirect
-                       }
-               } else {
-                       $targetTitle->invalidateCache(); // update histories
-               }
-               $destTitle->invalidateCache(); // update histories
-               # Check if this did anything
-               if ( !$count ) {
-                       $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
-
-                       return false;
-               }
-               # Update our logs
-               $logEntry = new ManualLogEntry( 'merge', 'merge' );
-               $logEntry->setPerformer( $this->getUser() );
-               $logEntry->setComment( $this->mComment );
-               $logEntry->setTarget( $targetTitle );
-               $logEntry->setParameters( array(
-                       '4::dest' => $destTitle->getPrefixedText(),
-                       '5::mergepoint' => $timestampLimit
-               ) );
-               $logId = $logEntry->insert();
-               $logEntry->publish( $logId );
 
                $targetLink = Linker::link(
                        $targetTitle,
@@ -490,11 +369,9 @@ class SpecialMergeHistory extends SpecialPage {
                $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
                        ->rawParams( $targetLink )
                        ->params( $destTitle->getPrefixedText() )
-                       ->numParams( $count )
+                       ->numParams( $mh->getMergedRevisionCount() )
                );
 
-               Hooks::run( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
-
                return true;
        }
 
index 078f032..5e08e51 100644 (file)
@@ -675,13 +675,14 @@ class PageArchive {
  * @ingroup SpecialPage
  */
 class SpecialUndelete extends SpecialPage {
-       private $mAction;
+       private $mAction;
        private $mTarget;
        private $mTimestamp;
        private $mRestore;
+       private $mRevdel;
        private $mInvert;
        private $mFilename;
-       private $mTargetTimestamp;
+       private $mTargetTimestamp;
        private $mAllowed;
        private $mCanView;
        private $mComment;
@@ -719,6 +720,7 @@ class SpecialUndelete extends SpecialPage {
                $posted = $request->wasPosted() &&
                        $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
                $this->mRestore = $request->getCheck( 'restore' ) && $posted;
+               $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
                $this->mInvert = $request->getCheck( 'invert' ) && $posted;
                $this->mPreview = $request->getCheck( 'preview' ) && $posted;
                $this->mDiff = $request->getCheck( 'diff' );
@@ -831,13 +833,42 @@ class SpecialUndelete extends SpecialPage {
                        } else {
                                $this->showFile( $this->mFilename );
                        }
-               } elseif ( $this->mRestore && $this->mAction == 'submit' ) {
-                       $this->undelete();
+               } elseif ( $this->mAction === "submit" ) {
+                       if ( $this->mRestore ) {
+                               $this->undelete();
+                       } elseif ( $this->mRevdel ) {
+                               $this->redirectToRevDel();
+                       }
+
                } else {
                        $this->showHistory();
                }
        }
 
+       /**
+        * Convert submitted form data to format expected by RevisionDelete and
+        * redirect the request
+        */
+       private function redirectToRevDel() {
+               $archive = new PageArchive( $this->mTargetObj );
+
+               $revisions = array();
+
+               foreach ( $this->getRequest()->getValues() as $key => $val ) {
+                       $matches = array();
+                       if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
+                               $revisions[ $archive->getRevision( $matches[1] )->getId() ] = 1;
+                       }
+               }
+               $query = array(
+                       "type" => "revision",
+                       "ids" => $revisions,
+                       "target" => wfUrlencode( $this->mTargetObj->getPrefixedText() )
+               );
+               $url = SpecialPage::getTitleFor( "RevisionDelete" )->getFullURL( $query );
+               $this->getOutput()->redirect( $url );
+       }
+
        function showSearchForm() {
                $out = $this->getOutput();
                $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
@@ -1356,7 +1387,20 @@ class SpecialUndelete extends SpecialPage {
                $out->addHTML( Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n" );
 
                if ( $haveRevisions ) {
-                       # The page's stored (deleted) history:
+                       # Show the page's stored (deleted) history
+
+                       if ( $this->getUser()->isAllowed( 'deleterevision' ) ) {
+                               $out->addHTML( Html::element(
+                                       'button',
+                                       array(
+                                               'name' => 'revdel',
+                                               'type' => 'submit',
+                                               'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
+                                       ),
+                                       $this->msg( 'showhideselectedversions' )->text()
+                               ) . "\n" );
+                       }
+
                        $out->addHTML( '<ul>' );
                        $remaining = $revisions->numRows();
                        $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
@@ -1466,13 +1510,9 @@ class SpecialUndelete extends SpecialPage {
                        $attribs['class'] = implode( ' ', $classes );
                }
 
-               // Revision delete links
-               $revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
-
-               $revisionRow = $this->msg( 'undelete-revision-row' )
+               $revisionRow = $this->msg( 'undelete-revision-row2' )
                        ->rawParams(
                                $checkBox,
-                               $revdlink,
                                $last,
                                $pageLink,
                                $userLink,
index da63075..6638fb7 100644 (file)
@@ -1789,14 +1789,14 @@ class User implements IDBAccessObject {
                        // ip-based limits
                        if ( isset( $limits['ip'] ) ) {
                                $ip = $this->getRequest()->getIP();
-                               $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
+                               $keys[wfMemcKey( 'limiter', $action, 'ip', $ip )] = $limits['ip'];
                        }
                        // subnet-based limits
                        if ( isset( $limits['subnet'] ) ) {
                                $ip = $this->getRequest()->getIP();
                                $subnet = IP::getSubnet( $ip );
                                if ( $subnet !== false ) {
-                                       $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
+                                       $keys[wfMemcKey( 'limiter', $action, 'subnet', $subnet )] = $limits['subnet'];
                                }
                        }
                }
index 7711d8f..3a929ac 100644 (file)
@@ -170,9 +170,9 @@ class Names {
                'gl' => 'galego',               # Galician
                'glk' => 'گیلکی',  # Gilaki
                'gn' => 'Avañe\'ẽ',  # Guaraní, Paraguayan
-               'gom' => 'à¤\97à¥\8bवा à¤\95à¥\8bà¤\82à¤\95णà¥\80 / Gova Konknni',      # Goan Konkani
-               'gom-deva' => 'à¤\97à¥\8bवा à¤\95à¥\8bà¤\82à¤\95णà¥\80',        # Goan Konkani (Devanagari script)
-               'gom-latn' => 'Gova Konknni',   # Goan Konkani (Latin script)
+               'gom' => 'à¤\97à¥\8bà¤\82यà¤\9aà¥\80 à¤\95à¥\8bà¤\82à¤\95णà¥\80 / Gõychi Konknni',     # Goan Konkani
+               'gom-deva' => 'à¤\97à¥\8bà¤\82यà¤\9aà¥\80 à¤\95à¥\8bà¤\82à¤\95णà¥\80',  # Goan Konkani (Devanagari script)
+               'gom-latn' => 'Gõychi Konknni',        # Goan Konkani (Latin script)
                'got' => '𐌲𐌿𐍄𐌹𐍃𐌺',    # Gothic
                'grc' => 'Ἀρχαία ἑλληνικὴ', # Ancient Greek
                'gsw' => 'Alemannisch', # Alemannic
index 0b35f9b..bcedf4a 100644 (file)
        "apisandbox-reset": "إفراغ",
        "apisandbox-retry": "أعد المحاولة",
        "apisandbox-examples": "أمثلة",
-       "apisandbox-results": "النتيجة",
+       "apisandbox-results": "النتائج",
        "apisandbox-request-url-label": "مسار الطلب:",
        "apisandbox-request-time": "وقت الطلب: $1",
        "booksources": "مصادر كتاب",
index 0e03f7f..3645bfc 100644 (file)
        "subject": "قونو:",
        "minoredit": "بو بیر کیچیک دَییشدیرمه‌دیر",
        "watchthis": "بو صفحه‌نی ایزله",
-       "savearticle": "صÙ\81Ø­Ù\87â\80\8cÙ\86Û\8c Ø³Ø§Ø®Ù\84ا",
+       "savearticle": "صÙ\81Ø­Ù\87â\80\8cÙ\86Û\8c Ø°Ø®Û\8cرÙ\87 Ø§Ø¦Øª",
        "preview": "اؤن‌گؤستریش",
        "showpreview": "سیناق گؤستریش",
        "showdiff": "دَییشیکلیکلری گؤستر",
index 949d107..2f22c6c 100644 (file)
        "botpasswords-created-body": "Пароль робата «$1» быў пасьпяхова створаны.",
        "botpasswords-updated-title": "Пароль робата абноўлены",
        "botpasswords-updated-body": "Пароль робата «$1» быў пасьпяхова абноўлены.",
+       "botpasswords-deleted-title": "Пароль робата выдалены",
+       "botpasswords-deleted-body": "Пароль робата «$1» быў выдалены.",
        "resetpass_forbidden": "Пароль ня можа быць зьменены",
        "resetpass-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
        "resetpass-submit-loggedin": "Зьмяніць пароль",
        "apisandbox-examples": "Прыклады",
        "apisandbox-results": "Вынікі",
        "apisandbox-request-url-label": "URL-адрас запыту:",
-       "apisandbox-request-time": "ЧаÑ\81 Ð°Ð¿Ñ\80аÑ\86оÑ\9eкÑ\96 Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\83: $1",
+       "apisandbox-request-time": "ЧаÑ\81 Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\83: {{PLURAL:$1|$1 Ð¼Ñ\81}}",
        "booksources": "Крыніцы кніг",
        "booksources-search-legend": "Пошук кніг",
        "booksources-isbn": "ISBN:",
index 86e4381..7d4dd42 100644 (file)
        "faqpage": "Project:Чѧстꙑ въпроси",
        "actions": "дѣиства",
        "namespaces": "имєнъ просторꙑ",
+       "variants": "обраꙁи",
        "navigation-heading": "плаваниѥ",
        "errorpagetitle": "блаꙁна",
        "returnto": "къ страници ⁖ $1 ⁖ въꙁвращєниѥ ⁙",
        "ncategories": "$1 {{PLURAL:$1|катигорїꙗ|катигорїи|катигорїѩ}}",
        "nlinks": "$1 {{PLURAL:$1|съвѧꙁь|съвѧꙁи|съвѧꙁии}}",
        "nmembers": "$1 {{PLURAL:$1|члѣнъ|члѣна|члѣни|члѣнъ}}",
+       "uncategorizedpages": "бєскатигорїинꙑ страницѧ",
+       "uncategorizedcategories": "бєскатигорїинꙑ катигорїѩ",
+       "uncategorizedimages": "бєскатигорїинꙑ дѣла",
+       "uncategorizedtemplates": "бєскатигорїинꙑ обраꙁьци",
+       "wantedcategories": "ноуждьни катигорїѩ",
+       "wantedpages": "ноуждьни страницѧ",
+       "wantedfiles": "ноуждьни дѣла",
+       "wantedtemplates": "ноуждьни обраꙁьци",
        "shortpages": "кратъкꙑ страницѧ",
        "longpages": "дльгꙑ страницѧ",
        "protectedpages-reason": "какъ съмꙑслъ",
index 2e1df41..09205e1 100644 (file)
        "mergehistory-empty": "No revisions can be merged.",
        "mergehistory-done": "$3 {{PLURAL:$3|revision|revisions}} of $1 {{PLURAL:$3|was|were}} merged into [[:$2]].",
        "mergehistory-fail": "Unable to perform history merge, please recheck the page and time parameters.",
-       "mergehistory-fail-toobig" : "Unable to perform history merge as more than the limit of $1 {{PLURAL:$1|revision|revisions}} would be moved.",
+       "mergehistory-fail-bad-timestamp": "Timestamp is invalid.",
+       "mergehistory-fail-invalid-source": "Source page is invalid.",
+       "mergehistory-fail-invalid-dest": "Destination page is invalid.",
+       "mergehistory-fail-no-change": "History merge did not merge any revisions. Please recheck the page and time parameters.",
+       "mergehistory-fail-permission": "Insufficient permissions to merge history.",
+       "mergehistory-fail-self-merge": "Source and destination pages are the same.",
+       "mergehistory-fail-timestamps-overlap": "Source revisions overlap or come after destination revisions.",
+       "mergehistory-fail-toobig": "Unable to perform history merge as more than the limit of $1 {{PLURAL:$1|revision|revisions}} would be moved.",
+       "mergehistory-warning-redirect-not-created": "",
        "mergehistory-no-source": "Source page $1 does not exist.",
        "mergehistory-no-destination": "Destination page $1 does not exist.",
        "mergehistory-invalid-source": "Source page must be a valid title.",
        "mergehistory-same-destination": "Source and destination pages cannot be the same",
        "mergehistory-reason": "Reason:",
        "mergehistory-revisionrow": "$1 ($2) $3 . . $4 $5 $6",
+       "mergehistory-redirect-text": "",
        "mergelog": "Merge log",
        "pagemerge-logentry": "merged [[$1]] into [[$2]] (revisions up to $3)",
        "revertmerge": "Unmerge",
        "undelete-error-long": "Errors were encountered while undeleting the file:\n\n$1",
        "undelete-show-file-confirm": "Are you sure you want to view the deleted revision of the file \"<nowiki>$1</nowiki>\" from $2 at $3?",
        "undelete-show-file-submit": "Yes",
-       "undelete-revision-row": "$1 $2 ($3) $4 . . $5 $6 $7 $8 $9",
+       "undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8",
        "namespace": "Namespace:",
        "invert": "Invert selection",
        "tooltip-invert": "Check this box to hide changes to pages within the selected namespace (and the associated namespace if checked)",
index 494033b..4ecff59 100644 (file)
                        "Cindie.Capel",
                        "ElGatoSaez",
                        "Joaquin1001",
-                       "YoViajo"
+                       "YoViajo",
+                       "Asierog"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "rcshowhidemine": "$1 mis ediciones",
        "rcshowhidemine-show": "Mostrar",
        "rcshowhidemine-hide": "Ocultar",
-       "rcshowhidecategorization": "$1 categorización de página",
+       "rcshowhidecategorization": "$1 categorización de páginas",
        "rcshowhidecategorization-show": "Mostrar",
        "rcshowhidecategorization-hide": "Ocultar",
        "rclinks": "Ver los últimos $1 cambios en los últimos $2 días.<br />$3",
        "apisandbox-retry": "Reintentar",
        "apisandbox-no-parameters": "Este módulo API no tiene parámetros.",
        "apisandbox-helpurls": "Enlaces de ayuda",
-       "apisandbox-examples": "Ejemplo",
+       "apisandbox-examples": "Ejemplos",
        "apisandbox-dynamic-parameters": "Parámetros adicionales",
        "apisandbox-dynamic-parameters-add-label": "Añadir parámetro:",
        "apisandbox-dynamic-parameters-add-placeholder": "Nombre del parámetro",
        "apisandbox-submit-invalid-fields-title": "Algunos campos no son válidos",
        "apisandbox-results": "Resultados",
        "apisandbox-sending-request": "Enviando pedido API...",
+       "apisandbox-loading-results": "Recibiendo resultados API...",
        "apisandbox-request-url-label": "URL solicitante:",
        "apisandbox-request-time": "Tiempo de solicitud: $1",
        "booksources": "Fuentes de libros",
        "listgrouprights-namespaceprotection-restrictedto": "Derechos de usuario para editar",
        "listgrants": "Subvenciones",
        "listgrants-grant": "Conceder",
-       "listgrants-rights": "Conceder",
+       "listgrants-rights": "Derechos",
        "trackingcategories": "Categorías de seguimiento",
        "trackingcategories-summary": "Esta página lista categorías de seguimiento que han sido generadas automáticamente por el software MediaWiki. Sus nombres pueden cambiarse editando su mensaje correspondiente en el espacio de nombres {{ns:8}}.",
        "trackingcategories-msg": "Categoría de seguimiento",
        "wlshowhideanons": "usuarios anónimos",
        "wlshowhidepatr": "ediciones verificadas",
        "wlshowhidemine": "mis ediciones",
-       "wlshowhidecategorization": "categorización de página",
+       "wlshowhidecategorization": "categorización de páginas",
        "watchlist-options": "Opciones de la lista de seguimiento",
        "watching": "Vigilando...",
        "unwatching": "Eliminando de la lista de seguimiento...",
index 0c3bbe5..8405d9f 100644 (file)
@@ -23,7 +23,8 @@
                        "Arkaitz Barnetik",
                        "Sator",
                        "Macofe",
-                       "Xð"
+                       "Xð",
+                       "Asierog"
                ]
        },
        "tog-underline": "Azpimarratu loturak:",
        "resetpass_submit": "Pasahitza definitu eta saioa hasi",
        "changepassword-success": "Zure pasahitza ondo aldatu da!",
        "changepassword-throttled": "Saioa hasteko saiakera gehiegi egin berri dituzu.\nBerriro saiatu aurretik $1 itxoin, mesedez.",
+       "botpasswords-label-create": "Sortu",
+       "botpasswords-label-update": "Eguneratu",
+       "botpasswords-label-delete": "Ezabatu",
        "resetpass_forbidden": "Ezin dira pasahitzak aldatu",
        "resetpass-no-info": "Orrialde honetara zuzenean sartzeko izena eman behar duzu.",
        "resetpass-submit-loggedin": "Pasahitza aldatu",
        "userrights": "Erabiltzaile baimenen kudeaketa",
        "userrights-lookup-user": "Erabiltzaile taldeak kudeatu",
        "userrights-user-editname": "Erabiltzaile izena idatzi:",
-       "editusergroup": "Erabiltzaile taldeak editatu",
+       "editusergroup": "{{GENDER:$1|Erabiltzaile}} taldeak editatu",
        "editinguser": "'''[[User:$1|$1]]''' $2 lankidearen erabiltzaile-eskubideak aldatzen",
        "userrights-editusergroup": "Erabiltzaile taldeak editatu",
        "saveusergroups": "Erabiltzaile taldeak gorde",
        "querypage-disabled": "Orrialde berezi hau desgaituta dago funtzionamendu arrazoiengatik.",
        "apihelp": "API laguntza",
        "apihelp-no-such-module": "Ez da \"$1\" modulua aurkitu.",
+       "apisandbox": "API proba orria",
+       "apisandbox-submit": "Egin eskaera",
+       "apisandbox-reset": "Garbitu",
+       "apisandbox-examples": "Adibideak",
+       "apisandbox-results": "Emaitzak",
        "booksources": "Iturri liburuak",
        "booksources-search-legend": "Liburuen bilaketa",
        "booksources-search": "Bilatu",
        "activeusers-noresult": "Ez da lankiderik aurkitu.",
        "listgrouprights": "Erabiltzaile talde eskumenak",
        "listgrouprights-summary": "Ondorengo zerrendak wikian dauden lankide taldeak agertzen dira, beraien eskubideekin.\nBadago [[{{MediaWiki:Listgrouprights-helppage}}|informazio osagarria]] banakako eskubideei buruz.",
-       "listgrouprights-key": "* <span class=\"listgrouprights-granted\">Eskubidea emanda</span>\n* <span class=\"listgrouprights-revoked\">Eskubidea kenduta</span>",
+       "listgrouprights-key": "Legenda\n* <span class=\"listgrouprights-granted\">Eskubidea emanda</span>\n* <span class=\"listgrouprights-revoked\">Eskubidea kenduta</span>",
        "listgrouprights-group": "Taldea",
        "listgrouprights-rights": "Eskumenak",
        "listgrouprights-helppage": "Help:Talde eskumenak",
        "wlshowlast": "Erakutsi azken $1 orduak, azken $2 egunak",
        "watchlist-hide": "Ezkutatu",
        "watchlist-submit": "Erakutsi",
-       "wlshowtime": "Erakutsi azkenak:",
+       "wlshowtime": "Erakusteko denboraldia:",
        "wlshowhideminor": "aldaketa txikiak",
        "wlshowhidebots": "bot-ak",
        "wlshowhideliu": "Erregistratutako erabiltzaileak",
index bc6a861..56605df 100644 (file)
@@ -48,7 +48,8 @@
                        "Macofe",
                        "Danialbehzadi",
                        "MRG90",
-                       "Mahdy Saffar"
+                       "Mahdy Saffar",
+                       "Arian Ar"
                ]
        },
        "tog-underline": "خط کشیدن زیر پیوندها:",
@@ -73,7 +74,7 @@
        "tog-enotifwatchlistpages": "اگر صفحه یا پرونده‌ای از فهرست پی‌گیری‌هایم ویرایش شد به من ایمیلی فرستاده شود",
        "tog-enotifusertalkpages": "هنگامی که در صفحهٔ بحث کاربری‌ام تغییری صورت می‌گیرد به من ایمیلی فرستاده شود",
        "tog-enotifminoredits": "برای تغییرات جزئی در صفحه‌ها و پرونده‌ها هم به من ایمیلی فرستاده شود",
-       "tog-enotifrevealaddr": "Ù\86شاÙ\86Û\8c Ù¾Ø³Øª Ø§Ù\84کترÙ\88Ù\86Û\8cÚ©Û\8c Ù\85Ù\86 Ø±Ø§ Ø¯Ø± Ø§Û\8cÙ\85Û\8cÙ\84â\80\8cÙ\87اÛ\8c Ø§Ø·Ù\84اعâ\80\8cرساÙ\86Û\8c Ù\87Ù\88Û\8cدا Ú¯Ø±Ø¯Ø¯",
+       "tog-enotifrevealaddr": "نشانی پست الکترونیکی من در ایمیل‌های اطلاع‌رسانی هویدا گردد",
        "tog-shownumberswatching": "شمار کاربران پی‌گیرندهٔ نمایش یابد",
        "tog-oldsig": "امضای کنونی:",
        "tog-fancysig": "امضا به صورت ویکی‌متن در نظر گرفته شود (بدون درج خودکار پیوند)",
        "create-this-page": "ایجاد این صفحه",
        "delete": "حذف",
        "deletethispage": "حذف این صفحه",
-       "undeletethispage": "بازگرداÙ\86ی این صفحه",
+       "undeletethispage": "احÛ\8cای این صفحه",
        "undelete_short": "احیای {{PLURAL:$1|یک ویرایش|$1 ویرایش}}",
        "viewdeleted_short": "نمایش {{PLURAL:$1|یک ویرایش حذف‌شده|$1 ویرایش حذف‌شده}}",
        "protect": "محافظت",
index 60c49d0..594c92e 100644 (file)
@@ -40,7 +40,7 @@
        "tog-hideminor": "הסתרת שינויים משניים ברשימת השינויים האחרונים",
        "tog-hidepatrolled": "הסתרת שינויים בדוקים ברשימת השינויים האחרונים",
        "tog-newpageshidepatrolled": "הסתרת דפים בדוקים ברשימת הדפים החדשים",
-       "tog-hidecategorization": "×\94סתרת ×\94×\95ספ×\95ת ×\95×\94סר×\95ת ×©×\9c ×\93פ×\99×\9d ×\9eקטגוריות",
+       "tog-hidecategorization": "×\94סתרת ×¡×\99×\95×\95×\92 ×\93פ×\99×\9d ×\9cקטגוריות",
        "tog-extendwatchlist": "הרחבת רשימת המעקב כך שתציג את כל השינויים, לא רק את השינויים האחרונים בכל דף",
        "tog-usenewrc": "קיבוץ השינויים לפי דף בשינויים האחרונים וברשימת המעקב",
        "tog-numberheadings": "מספור כותרות אוטומטי",
@@ -71,7 +71,7 @@
        "tog-watchlistreloadautomatically": "רענון אוטומטי של רשימת המעקב בכל פעם שמסנן משתנה (נדרש JavaScript)",
        "tog-watchlisthideanons": "הסתרת עריכות של משתמשים אנונימיים ברשימת המעקב",
        "tog-watchlisthidepatrolled": "הסתרת עריכות בדוקות ברשימת המעקב",
-       "tog-watchlisthidecategorization": "×\94סתרת ×\94×\95ספ×\95ת ×\95×\94סר×\95ת ×©×\9c ×\93פ×\99×\9d ×\9eקטגוריות",
+       "tog-watchlisthidecategorization": "×\94סתרת ×¡×\99×\95×\95×\92 ×\93פ×\99×\9d ×\9cקטגוריות",
        "tog-ccmeonemails": "לשלוח אליי העתקים של הודעות דואר אלקטרוני ששלחתי למשתמשים אחרים",
        "tog-diffonly": "ביטול הצגת תוכן הדף מתחת להשוואות הגרסאות",
        "tog-showhiddencats": "הצגת קטגוריות מוסתרות",
        "wlshowhideanons": "משתמשים אנונימיים",
        "wlshowhidepatr": "עריכות בדוקות",
        "wlshowhidemine": "עריכות שלי",
-       "wlshowhidecategorization": "×\94×\95ספ×\95ת ×\95×\94סר×\95ת ×©×\9c ×\93פ×\99×\9d ×\9eקטגוריות",
+       "wlshowhidecategorization": "ס×\99×\95×\95×\92 ×\93פ×\99×\9d ×\9cקטגוריות",
        "watchlist-options": "אפשרויות ברשימת המעקב",
        "watching": "בהוספה לרשימת המעקב…",
        "unwatching": "בהסרה מרשימת המעקב…",
index f645a28..092734f 100644 (file)
        "preview": "Pêşdîtin",
        "showpreview": "Pêşdîtinê nîşan bide",
        "showdiff": "Guherandinan nîşan bide",
-       "anoneditwarning": "<strong>Hişyarî:<strong> Tu netêketî yî! Navnîşana IP'ya te wê di dîroka guherandina vê rûpelê de bê tomarkirin. Heke tu <strong>[$1 têkevî]</strong>, li gel sûdên te yên din guhertinên ku tu bikî jî wê ji nasnavê te re bê atfkirin.",
+       "anoneditwarning": "<strong>Hişyarî:<strong> Tu netêketî yî! Navnîşana IP'ya te wê di dîroka guherandina vê rûpelê de bê tomarkirin. Heke tu <strong>[$1 têkevî]</strong> an jî  <strong>[$2 hesabekî çêbikî]</strong>, li gel sûdên te yên din guhertinên ku tu bikî jî wê ji nasnavê te re bê atfkirin.",
        "anonpreviewwarning": "''Tu ne têketî yî. Tomarkirin wê navnîşana IP'ya te di dîroka guhertinan de nîşan bide.''",
        "missingsummary": "<span style=\"color:#990000;\">'''Zanibe:'''</span> Te nivîsekî kurt ji bo guherandinê ra nenivîsand. Eger tu niha carekî din li Tomar xê, guherandinê te vê nivîsekî kurt yê were tomarkirin.",
        "missingcommenttext": "Ji kerema xwe kurteya naverokê li jêr binivisîne.",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[{{PLURAL:$1|bikarhênerek|$1 bikarhêner}} vê rûpelê {{PLURAL:$1|dişopîne|dişopînin}}.]",
        "rc_categories_any": "Qet",
+       "rc-change-size-new": "Piştî guhertinê $1 {{PLURAL:$1|bayt}}",
        "newsectionsummary": "/* $1 */ beşeke nû",
        "rc-enhanced-expand": "Hûragahiyan nîşan bide",
        "rc-enhanced-hide": "Kitûmatan veşêre",
        "import-upload": "Daneyên XMLê bar bike",
        "importlogpage": "Têketina tevlîkirinê",
        "javascripttest": "JavaScript tê testkirin",
-       "tooltip-pt-userpage": "Rûpela min",
+       "tooltip-pt-userpage": "Rûpela {{GENDER:|Te}}",
        "tooltip-pt-anonuserpage": "Rûpela bikarhênerê ji bo navnîşana ÎP ku tu sererast dikî wekî",
-       "tooltip-pt-mytalk": "Gotûbêja min",
-       "tooltip-pt-preferences": "Hevyazên min",
+       "tooltip-pt-mytalk": "Gotûbêja {{GENDER:|Te}}",
+       "tooltip-pt-preferences": "Tercîhên {{GENDER:|te}}",
        "tooltip-pt-watchlist": "The list of pages you",
-       "tooltip-pt-mycontris": "Lîsteya beşdariyên min",
+       "tooltip-pt-mycontris": "Lîsteyekê beşdariyên {{GENDER:|te}}",
+       "tooltip-pt-login": "Têketina we tê teşwîqkirin; lêbelê, en ne elzem e",
        "tooltip-pt-logout": "Derkeve",
        "tooltip-ca-talk": "Gotûbêj li ser rûpela naverokê",
        "tooltip-ca-edit": "Vê rûpelê biguherîne",
        "tooltip-t-recentchangeslinked": "Recent changes in pages linking to this page",
        "tooltip-feed-rss": "RSS feed'ên ji bo rûpelê",
        "tooltip-feed-atom": "Atom feed'ên ji bo vê rûpelê",
-       "tooltip-t-contributions": "Lîsteya beşdariyên bikarhêner bibîne",
+       "tooltip-t-contributions": "Lîsteyekî beşdariyên {{GENDER:$1|vê bikarhênerê}} bibîne",
        "tooltip-t-emailuser": "Jê re name bişîne",
        "tooltip-t-info": "Bêhtir agahî di derbarê vê rûpelê de",
        "tooltip-t-upload": "Dosyeyan bar bike",
index aabcdc1..5bb32c1 100644 (file)
        "otherlanguages": "In aliis linguis",
        "redirectedfrom": "(Redirectum de $1)",
        "redirectpagesub": "Pagina redirectionis",
-       "lastmodifiedat": "Novissima mutatio die $2 hora $1 facta.",
+       "lastmodifiedat": "Novissima mutatio die $1 hora $2 facta.",
        "viewcount": "Haec pagina iam vista est {{PLURAL:$1|semel|$1 vices}}.",
        "protectedpage": "Pagina protecta",
        "jumpto": "Salire ad:",
index 416eafc..6090f4f 100644 (file)
        "movenosubpage": "Dës Säit huet keng Ënnersäiten.",
        "movereason": "Grond:",
        "revertmove": "zréck réckelen",
-       "delete_and_move_text": "== Läsche vun der Destinatiounssäit néideg ==\nD'Säit \"[[:$1]]\" existéiert schonn. \nWëll Dir se läsche fir d'Réckelen ze erméiglechen?",
+       "delete_and_move_text": "D'Zilsäit \"[[:$1]]\" gëtt et schonn. \nWëll Dir se läsche fir d'Réckelen ze erméiglechen?",
        "delete_and_move_confirm": "Jo, läsch d'Säit",
        "delete_and_move_reason": "Geläscht fir Plaz ze maache fir \"[[$1]]\" heihin ze réckelen",
        "selfmove": "Source- an Destinatiounsnumm sinn dselwecht; eng Säit kann net op sech selwer geréckelt ginn.",
index 8c2b0ef..2a4a566 100644 (file)
        "searchprofile-images-tooltip": "Søk etter filer",
        "searchprofile-everything-tooltip": "Søk i alt innhold (inkldert diskusjonssider)",
        "searchprofile-advanced-tooltip": "Søk i visse navnerom",
-       "search-result-size": "$1 ({{PLURAL:$2|ett|$2}} ord)",
+       "search-result-size": "$1 ({{PLURAL:$2|ett ord|$2 ord}})",
        "search-result-category-size": "{{PLURAL:$1|1 medlem|$1 medlemmer}} ({{PLURAL:$2|1 underkategori|$2 underkategorier}}, {{PLURAL:$3|1 fil|$3 filer}})",
        "search-redirect": "(omdirigering $1)",
        "search-section": "(avsnitt $1)",
        "sp-contributions-toponly": "Vis kun endringer som er gjeldende revisjoner",
        "sp-contributions-newonly": "Bare vis bidrag som er sideopprettinger",
        "sp-contributions-submit": "Søk",
-       "whatlinkshere": "Lenker hit",
+       "whatlinkshere": "Hva lenker hit",
        "whatlinkshere-title": "Sider som lenker til «$1»",
        "whatlinkshere-page": "Side:",
        "linkshere": "Følgende sider lenker til '''[[:$1]]''':",
        "tooltip-search-fulltext": "Søk etter sider som innholder denne teksten",
        "tooltip-p-logo": "Gå til hovedsiden",
        "tooltip-n-mainpage": "Gå til hovedsiden",
-       "tooltip-n-mainpage-description": "Gå til hovedsiden",
+       "tooltip-n-mainpage-description": "Besøk hovedsiden",
        "tooltip-n-portal": "Om prosjektet, hva du kan gjøre, hvor du kan finne ting",
        "tooltip-n-currentevents": "Finn bakgrunnsinformasjon om aktuelle hendelser",
        "tooltip-n-recentchanges": "Liste over siste endringer på wikien.",
        "metadata-help": "Denne filen inneholder tilleggsinformasjon, antagligvis lagt til av digitalkameraet eller skanneren brukt til å lage eller digitalisere det.\nHvis filen har blitt forandret fra utgangspunktet, kan enkelte detaljer være unøyaktige.",
        "metadata-expand": "Vis utvidede detaljer",
        "metadata-collapse": "Skjul utvidede detaljer",
-       "metadata-fields": "Bildemetadatafelt listet i denne meldingen inkluderes på bildesiden når metadatatabellen er slått sammen.\nAndre vil skjules som standard.\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",
+       "metadata-fields": "Bildemetadatafelt listet i denne meldingen inkluderes på bildesiden når metadatatabellen har kollapset.\nAndre vil skjules som standard.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "exif-imagewidth": "Bredde",
        "exif-imagelength": "Høyde",
        "exif-bitspersample": "Bits per komponent",
index 74b16e5..32f5b49 100644 (file)
        "apisandbox-submit": "Fazer o pedido",
        "apisandbox-reset": "Limpar",
        "apisandbox-examples": "Exemplos",
-       "apisandbox-results": "Resultado",
+       "apisandbox-results": "Resultados",
        "apisandbox-request-url-label": "URL do pedido:",
        "apisandbox-request-time": "Tempo de processamento: $1",
        "booksources": "Fontes bibliográficas",
index fec3079..86be090 100644 (file)
        "mergehistory-empty": "Used in [[Special:MergeHistory]].",
        "mergehistory-done": "Success message shown on [[Special:MergeHistory]].\n* $1 - link to target page\n* $2 - destination page title\n* $3 - number of revisions which succeeded to merge",
        "mergehistory-fail": "Used as error message in [[Special:MergeHistory]].",
-       "mergehistory-fail-toobig": "Used as error message in [[Special:MergeHistory]].\n* $1 - maximum allowed number of revisions that can be moved",
+       "mergehistory-fail-bad-timestamp": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-invalid-source": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-invalid-dest": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-no-change": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-permission": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-self-merge": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-timestamps-overlap": "Used as error message in [[Special:MergeHistory]] API.",
+       "mergehistory-fail-toobig": "Used as error message in [[Special:MergeHistory]] and API.\n* $1 - maximum allowed number of revisions that can be moved",
+       "mergehistory-warning-redirect-not-created": "Used as warning message in [[Special:MergeHistory]] API.",
        "mergehistory-no-source": "Used as error message in [[Special:MergeHistory]].\n* $1 - source page title\nSee also:\n* {{msg-mw|mergehistory-invalid-source}}\n* {{msg-mw|mergehistory-invalid-destination}}\n* {{msg-mw|mergehistory-no-destination}}\n* {{msg-mw|mergehistory-same-destination}}",
        "mergehistory-no-destination": "Used as error message in [[Special:MergeHistory]].\n* $1 - destination page title\nSee also:\n* {{msg-mw|mergehistory-invalid-source}}\n* {{msg-mw|mergehistory-no-source}}\n* {{msg-mw|mergehistory-invalid-destination}}\n* {{msg-mw|mergehistory-same-destination}}",
        "mergehistory-invalid-source": "Used as error message in [[Special:MergeHistory]].\n\nSee also:\n* {{msg-mw|mergehistory-no-source}}\n* {{msg-mw|mergehistory-invalid-destination}}\n* {{msg-mw|mergehistory-no-destination}}\n* {{msg-mw|mergehistory-same-destination}}",
        "mergehistory-same-destination": "Error message shown on [[Special:MergeHistory]] when the user entered the same page title to both source and destination\n\nSee also:\n* {{msg-mw|mergehistory-invalid-source}}\n* {{msg-mw|mergehistory-no-source}}\n* {{msg-mw|mergehistory-invalid-destination}}\n* {{msg-mw|mergehistory-no-destination}}",
        "mergehistory-reason": "{{Identical|Reason}}",
        "mergehistory-revisionrow": "{{Optional}}\nA revision row in the merge history page. Parameters:\n* $1 - a radio button to indicate a merge point\n* $2 - a link to the last revision of a page ({{msg-mw|Last}})\n* $3 - a page link\n* $4 - a user link\n* $5 - a revision size\n* $6 - a revision comment",
+       "mergehistory-redirect-text": "{{ignored}}The text that's added to a redirected page when that redirect is created as part of a history merge.",
        "mergelog": "{{doc-logpage}}\n\nThis is the name of a log of merge actions done on [[Special:MergeHistory]]. This special page and this log is not enabled by default.",
        "pagemerge-logentry": "{{ignored}}This is a ''logentry'' message only used on IRC.\n\nParameters:\n* $1 - the page name of the source of the content to be merged\n* $2 - the page into which the content is merged\n* $3 - a timestamp of limit\n\nThe log and its associated special page 'MergeHistory' is not enabled by default.\n\nPlease note that the parameters in a log entry will appear in the log only in the default language of the wiki. View [[Special:Log]] for examples on translatewiki.net with English default language.",
        "revertmerge": "Used as link text",
        "undelete-error-long": "Used as error message. Parameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Undelete-error-short}}",
        "undelete-show-file-confirm": "A confirmation message shown on [[Special:Undelete]] when the request does not contain a valid token (e.g. when a user clicks a link received in mail).\n\nParameters:\n* $1 - the name of the file being undeleted\n* $2 - the date of the displayed revision\n* $3 - the time of the displayed revision\n{{Identical|Are you sure you want to view the deleted revision of the file...}}",
        "undelete-show-file-submit": "{{Identical|Yes}}",
-       "undelete-revision-row": "{{Optional}}\nA revision row in the undelete page. Parameters:\n* $1 is a checkBox to indicate whether to restore this specific revision\n* $2 is a link to the revision\n* $3 is a link to the last revision of a page ({{msg-mw|last}})\n* $4 is a link to the page\n* $5 is a link to the revision's user\n* $6 is the revision's minor edit identifier\n* $7 is the revision size\n* $8 is the revision comment\n* $9 is the revision's tags",
+       "undelete-revision-row2": "{{Optional}}\nA revision row in the undelete page. Parameters:\n* $1 is a checkBox to indicate whether to restore this specific revision\n* $2 is a link to the last revision of a page ({{msg-mw|last}})\n* $3 is a link to the page\n* $4 is a link to the revision's user\n* $5 is the revision's minor edit identifier\n* $6 is the revision size\n* $7 is the revision comment\n* $8 is the revision's tags",
        "namespace": "This message is located at [[Special:Contributions]].\n{{Identical|Namespace}}",
        "invert": "Displayed in [[Special:RecentChanges|RecentChanges]], [[Special:RecentChangesLinked|RecentChangesLinked]] and [[Special:Watchlist|Watchlist]].\n\nThis message means \"Invert selection of namespace\".\n\nThis message has a tooltip {{msg-mw|tooltip-invert}}\n{{Identical|Invert selection}}",
        "tooltip-invert": "Used in [[Special:Recentchanges]] as a tooltip for the invert checkbox. See also the message {{msg-mw|invert}}",
index 8c05fca..8d18e6d 100644 (file)
        "thu": "чет-čet",
        "fri": "pet-пет",
        "sat": "sub-суб",
-       "january": "januar-сијечањ",
-       "february": "februar-вељача",
-       "march": "mart-ожујак",
-       "april": "april-травањ",
-       "may_long": "maj-свибањ",
-       "june": "jun-липањ",
-       "july": "jul-српањ",
-       "august": "avgust-коловоз",
-       "september": "septembar-рујан",
-       "october": "oktobar-листопад",
-       "november": "студени-novembar",
-       "december": "decembar-просинац",
-       "january-gen": "januara-сијечња",
-       "february-gen": "februara-вељаче",
-       "march-gen": "marta-ожујка",
-       "april-gen": "aprila-травња",
-       "may-gen": "маја-свибња",
-       "june-gen": "junа-липња",
-       "july-gen": "jula-српња",
-       "august-gen": "augusta-коловоза",
-       "september-gen": "septembra-рујна",
-       "october-gen": "oktobra-листопада",
-       "november-gen": "студенога-novembra",
-       "december-gen": "decembra-просинца",
-       "jan": "jan-сиј",
-       "feb": "feb-вељ",
-       "mar": "mar-ожу",
-       "apr": "apr-тра",
-       "may": "maj-сви",
-       "jun": "jun-лип",
-       "jul": "jul-срп",
-       "aug": "aug-кол",
-       "sep": "sep-руј",
-       "oct": "okt-лис",
-       "nov": "сту-nov",
-       "dec": "dec-про",
-       "january-date": "$1. januar",
-       "february-date": "$1. februar",
-       "march-date": "$1. mart",
-       "april-date": "$1. april",
-       "may-date": "$1. maj",
-       "june-date": "$1. jun",
-       "july-date": "$1. jul",
-       "august-date": "$1. august",
-       "september-date": "$1. septembar",
-       "october-date": "$1. oktobar",
-       "november-date": "$1. novembar",
-       "december-date": "$1. decembar",
+       "january": "januar",
+       "february": "februar",
+       "march": "mart",
+       "april": "april",
+       "may_long": "maj",
+       "june": "juni",
+       "july": "juli",
+       "august": "august",
+       "september": "septembar",
+       "october": "oktobar",
+       "november": "novembar",
+       "december": "decembar",
+       "january-gen": "januara",
+       "february-gen": "februara",
+       "march-gen": "marta",
+       "april-gen": "aprila",
+       "may-gen": "maja",
+       "june-gen": "juna",
+       "july-gen": "jula",
+       "august-gen": "augusta",
+       "september-gen": "septembra",
+       "october-gen": "oktobra",
+       "november-gen": "novembra",
+       "december-gen": "decembra",
+       "jan": "jan.",
+       "feb": "feb.",
+       "mar": "mar.",
+       "apr": "apr.",
+       "may": "maj",
+       "jun": "jun.",
+       "jul": "jul.",
+       "aug": "aug.",
+       "sep": "sep.",
+       "oct": "okt.",
+       "nov": "nov.",
+       "dec": "dec.",
+       "january-date": "$1. januara",
+       "february-date": "$1. februara",
+       "march-date": "$1. marta",
+       "april-date": "$1. aprila",
+       "may-date": "$1. maja",
+       "june-date": "$1. juna",
+       "july-date": "$1. jula",
+       "august-date": "$1. augusta",
+       "september-date": "$1. septembra",
+       "october-date": "$1. oktobra",
+       "november-date": "$1. novembra",
+       "december-date": "$1. decembra",
        "pagecategories": "{{PLURAL:$1|Kategorija|Kategorije}}",
        "category_header": "Stranice u kategoriji \"$1\"",
        "subcategories": "Potkategorije",
        "noindex-category": "Neindeksirane stranice",
        "broken-file-category": "Stranice sa neispravnim linkovima do datoteka",
        "about": "O...",
-       "article": "Stranica sadržaja (članak)",
+       "article": "Stranica sa sadržajem",
        "newwindow": "(otvara se u novom prozoru)",
-       "cancel": "Odustani - Одустани",
+       "cancel": "Otkaži",
        "moredotdotdot": "Još...",
        "morenotlisted": "Ovaj spisak nije kompletan.",
        "mypage": "Moja stranica",
        "errorpagetitle": "Greška - Грешка",
        "returnto": "Povratak na $1.",
        "tagline": "Izvor: {{SITENAME}}",
-       "help": "Pomoć / Помоћ",
+       "help": "Pomoć",
        "search": "Traži / Тражи",
        "searchbutton": "Traži",
        "go": "Idi / Иди",
        "history": "Historija stranice",
        "history_short": "Historija",
        "updatedmarker": "promjene od moje zadnje posjete",
-       "printableversion": "Za štampanje / За штампање",
+       "printableversion": "Verzija za ispis",
        "permalink": "Trajni link",
        "print": "Štampa",
        "view": "Vidi",
        "talkpagelinktext": "Razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Lični alati",
-       "articlepage": "Pogledaj stranicu sa sadržajem (članak)",
+       "articlepage": "Vidi stranicu sa sadržajem",
        "talk": "Razgovor",
        "views": "Pregledi",
-       "toolbox": "Alatke / Алатке",
+       "toolbox": "Alati",
        "userpage": "Pogledaj korisničku stranicu - Погледај корисничку страницу",
        "projectpage": "Pogledajte stranicu projekta",
        "imagepage": "Vidi stranicu datoteke/fajla",
        "viewhelppage": "Pogledajte stranicu za pomoć",
        "categorypage": "Pogledaj stranicu kategorije",
        "viewtalkpage": "Pogledajte raspravu",
-       "otherlanguages": "Drugi jezici / Други језици",
+       "otherlanguages": "Na drugim jezicima",
        "redirectedfrom": "(Preusmjereno sa $1)",
        "redirectpagesub": "Preusmjeri stranicu",
        "redirectto": "Preusmjerenje na:",
        "copyrightpage": "{{ns:project}}:Autorska_prava",
        "currentevents": "Trenutni događaji",
        "currentevents-url": "Project:Novosti",
-       "disclaimers": "Odricanje odgovornosti",
-       "disclaimerpage": "Project:Uslovi korištenja, pravne napomene i odricanje odgovornosti",
+       "disclaimers": "Odricanje od odgovornosti",
+       "disclaimerpage": "Project:Opće odricanje od odgovornosti",
        "edithelp": "Pomoć pri uređivanju",
        "helppage-top-gethelp": "Pomoć",
        "mainpage": "Glavna stranica / Главна страница",
        "policy-url": "Project:Pravila",
        "portal": "Portal zajednice",
        "portal-url": "Project:Portal_zajednice",
-       "privacy": "Politika privatnosti - Политика приватности",
+       "privacy": "Politika privatnosti",
        "privacypage": "Project:Pravila o anonimnosti",
        "badaccess": "Greška pri odobrenju",
        "badaccess-group0": "Nije vam dozvoljeno izvršiti akciju koju ste zahtjevali.",
        "sort-descending": "Poredaj opadajuće",
        "sort-ascending": "Poredaj rastuće",
        "nstab-main": "Članak / Чланак",
-       "nstab-user": "Korisnik / Корисник",
+       "nstab-user": "Stranica korisnika",
        "nstab-media": "Mediji",
        "nstab-special": "Posebna stranica",
        "nstab-project": "Stranica projekta",
        "nstab-image": "Datoteka",
        "nstab-mediawiki": "Poruka / Порука",
        "nstab-template": "Šablon / Шаблон",
-       "nstab-help": "Pomoć / Помоћ",
+       "nstab-help": "Pomoć",
        "nstab-category": "Kategorija / Категорија",
        "mainpage-nstab": "Glavna stranica / Главна страница",
        "nosuchaction": "Nema takve akcije",
        "resetpass_submit": "Odredi lozinku i prijavi se",
        "changepassword-success": "Vaša šifra je uspiješno promjenjena! Prijava u toku...",
        "changepassword-throttled": "Previše puta ste se pokušali prijaviti.\nMolimo Vas da sačekate $1 prije nego što pokušate ponovo.",
+       "botpasswords-label-cancel": "Otkaži",
        "resetpass_forbidden": "Šifre ne mogu biti promjenjene",
        "resetpass-no-info": "Morate biti prijavljeni da bi ste pristupili ovoj stranici direktno.",
        "resetpass-submit-loggedin": "Promijeni lozinku",
-       "resetpass-submit-cancel": "Odustani",
+       "resetpass-submit-cancel": "Otkaži",
        "resetpass-wrong-oldpass": "Privremena ili trenutna lozinka nije valjana.\nMožda ste već uspješno promijenili Vašu lozinku ili ste tražili novu privremenu lozinku.",
        "resetpass-recycled": "Molimo resetirajte vašu lozinku/zaporku u nešto drugo od vaše trenutne lozinke/zaporke.",
        "resetpass-temp-emailed": "Prijavili ste se sa privremenim kodom iz e-pošte.\nDa biste završili prijavljivanje morate postaviti novu lozinku ovde:",
        "hr_tip": "Horizontalna linija (koristite rijetko)",
        "summary": "Sažetak:",
        "subject": "Tema:",
-       "minoredit": "Mala izmjena - Мала измена",
-       "watchthis": "Prati / Прати",
-       "savearticle": "Sačuvaj - Сачувај",
-       "preview": "Pretpregled / Претпреглед",
-       "showpreview": "Pretpregled - Претпреглед",
-       "showdiff": "Prikaži izmjene - Прикажи измене",
+       "minoredit": "Ovo je manje uređenje",
+       "watchthis": "Motri ovu stranicu",
+       "savearticle": "Spremi stranicu",
+       "preview": "Pregled",
+       "showpreview": "Prikaži pregled",
+       "showdiff": "Prikaži izmjene",
        "blankarticle": "<strong>Upozorenje:</strong> Napravili ste praznu stranicu.\nAko ponovno kliknete \"{{int:savearticle}}\", napravit ćete praznu stranicu bez sadržaja.",
        "anoneditwarning": "<strong>Upozorenje:</strong> Niste prijavljeni. \nVaša IP adresa će biti javno vidljiva ako napravite neku izmjenu. Ako se <strong>[$1 prijavite]</strong> ili <strong>[$2 napravite račun]</strong>, vaše izmjene će biti pripisane vašem korisničkom imenu, zajedno sa drugim pogodnostima.",
        "anonpreviewwarning": "''Niste prijavljeni. Vaša IP adresa će biti zabilježena u historiji ove stranice.''",
        "viewprevnext": "Pogledaj ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''Postoji stranica pod nazivom \"[[:$1]]\" na ovoj wiki'''",
        "searchmenu-new": "<strong>Napravi stranicu \"[[:$1]]\" na ovoj wiki!</strong> {{PLURAL:$2|0=|Pogledajte također straniu pronađenu vašom pretragom.|Pogledajte također i vaše rezultate pretrage.}}",
-       "searchprofile-articles": "Stranice sadržaja",
+       "searchprofile-articles": "Stranice sa sadržajem",
        "searchprofile-images": "Multimedija",
        "searchprofile-everything": "Sve",
        "searchprofile-advanced": "Napredno",
        "prefs-edits": "Broj izmjena:",
        "prefsnologintext2": "Molimo Vas prijavite se da biste promijenili postavke.",
        "prefs-skin": "Izgled (skin)",
-       "skin-preview": "Pretpregled",
+       "skin-preview": "Pregled",
        "datedefault": "Bez preferenci",
        "prefs-labs": "Eksperimentalne mogućnosti",
        "prefs-user-pages": "Korisničke stranice",
        "prefs-personal": "Korisnički profil",
        "prefs-rc": "Podešavanje nedavnih izmjena",
-       "prefs-watchlist": "Praćene stranice / Списак надгледања",
+       "prefs-watchlist": "Lista motrenja",
        "prefs-editwatchlist": "Uredi popis praćenja",
        "prefs-editwatchlist-label": "Uredi unose na popisu praćenja:",
        "prefs-editwatchlist-edit": "Vidite i uklonite naslove na vašem popisu praćenja",
        "prefs-timeoffset": "Vremenska razlika",
        "prefs-advancedediting": "Opće opcije",
        "prefs-editor": "Uređivač",
-       "prefs-preview": "Pretpregled",
+       "prefs-preview": "Pregled",
        "prefs-advancedrc": "Napredne opcije",
        "prefs-advancedrendering": "Napredne opcije",
        "prefs-advancedsearchoptions": "Napredne opcije",
        "right-createtalk": "Pravljenje stranica za razgovor",
        "right-createaccount": "Pravljenje korisničkog računa",
        "right-minoredit": "Označavanje izmjena kao malih",
-       "right-move": "Preusmjeravanje stranica",
+       "right-move": "Premještanje stranica",
        "right-move-subpages": "Preusmjeravanje stranica sa svim podstranicama",
        "right-move-rootuserpages": "Premještanje stranica osnovnih korisnika",
        "right-move-categorypages": "Pomakni stranice kategorije",
        "rc-enhanced-hide": "Sakrij detalje",
        "rc-old-title": "prvobitno kreirano kao \"$1\"",
        "recentchangeslinked": "Srodne izmjene / Сродне измене",
-       "recentchangeslinked-feed": "Srodne izmjene",
-       "recentchangeslinked-toolbox": "Srodne izmjene",
-       "recentchangeslinked-title": "Srodne promjene sa \"$1\"",
+       "recentchangeslinked-feed": "Vezane izmjene",
+       "recentchangeslinked-toolbox": "Vezane izmjene",
+       "recentchangeslinked-title": "Izmjene vezane s \"$1\"",
        "recentchangeslinked-summary": "Ova posebna stranica prikazuje promjene na povezanim stranicama.\nStranice koje su na vašem [[Special:Watchlist|spisku praćenja]] su '''podebljane'''.",
        "recentchangeslinked-page": "Naslov stranice:",
        "recentchangeslinked-to": "Pokaži promjene stranica koji su povezane sa datom stranicom",
        "upload-http-error": "Desila se HTTP greška: $1",
        "upload-copy-upload-invalid-domain": "Kopije postavljanja nisu dostupni na ovom domenu.",
        "upload-dialog-title": "Postavi datoteku",
-       "upload-dialog-button-cancel": "Odustani",
+       "upload-dialog-button-cancel": "Otkaži",
        "upload-dialog-button-done": "Urađeno",
        "upload-dialog-button-save": "Snimi",
        "upload-dialog-button-upload": "Postavi",
        "statistics-header-edits": "Statistike izmjena",
        "statistics-header-users": "Statistike korisnika",
        "statistics-header-hooks": "Ostale statistike",
-       "statistics-articles": "Stranice sadržaja",
+       "statistics-articles": "Stranice sa sadržajem",
        "statistics-pages": "Stranice",
        "statistics-pages-desc": "Sve stranice na wikiju, uključujući stranice za razgovor, preusmjerenja itd.",
        "statistics-files": "Broj postavljenih datoteka",
        "trackingcategories-disabled": "Kategorija je onemogućena",
        "mailnologin": "Nema adrese za slanje",
        "mailnologintext": "Morate biti [[Special:UserLogin|prijavljeni]] i imati ispravnu adresu e-pošte u vašim [[Special:Preferences|podešavanjima]] da biste slali e-poštu drugim korisnicima.",
-       "emailuser": "Pošalji E-mail ovom korisniku",
+       "emailuser": "Pošalji e-mail ovom korisniku",
        "emailuser-title-target": "Slanje e-maila {{GENDER:$1|korisniku|korisnici|korisniku}}",
        "emailuser-title-notarget": "Slanje e-maila korisniku",
        "emailpagetext": "Možete da koristite donji obrazac da pošaljete e-mail {{GENDER:$1|ovom korisniku|ovoj korisnici|ovom korisniku|}}.\nE-mail koju ste uneli u vašim [[Special:Preferences|postavkama]] će se prikazati u polju \"Od:\", tako da će primalac moći da vam odgovori direktno.",
        "emailuserfooter": "Ovu e-poruku {{GENDER:$1|poslao|poslala}} je $1 {{GENDER:$2|korisniku|korisnici}} $2 pomoću funkcije \"{{int:emailuser}}\" s projekta {{SITENAME}}.",
        "usermessage-summary": "Ostavljanje sistemske poruke.",
        "usermessage-editor": "Sistem za poruke",
-       "watchlist": "Spisak praćenja / Списак праћења",
+       "watchlist": "Lista motrenja",
        "mywatchlist": "Lista motrenja",
        "watchlistfor2": "Za $1 $2",
        "nowatchlist": "Nemate ništa na svom spisku praćenih članaka.",
        "removewatch": "Ukloni sa spiska praćenja",
        "removedwatchtext": "Stranica „[[:$1]]“ i njena stranica za razgovor je uklonjena s vašeg [[Special:Watchlist|spiska nadgledanja]].",
        "removedwatchtext-short": "Stranica \"$1\" je uklonjena sa vašeg spiska praćenja.",
-       "watch": "Prati / Прати",
-       "watchthispage": "Prati / Прати",
+       "watch": "Motri",
+       "watchthispage": "Motri ovu stranicu",
        "unwatch": "Prekini praćenje",
-       "unwatchthispage": "Ukinite praćenje",
-       "notanarticle": "Nije članak",
+       "unwatchthispage": "Prestani motriti",
+       "notanarticle": "Nije stranica sa sadržajem",
        "notvisiblerev": "Posljednja izmjena drugog korisnika je bila izbrisana",
        "watchlist-details": "{{PLURAL:$1|$1 stranica|$1 stranice|$1 stranica }} na vašem spisku praćenja, ne računajući posebno stranice za razgovor.",
        "wlheader-enotif": "* Obavještavanje e-poštom je omogućeno.",
        "sp-contributions-toponly": "Prikaži samo izmjene koje su posljednje revizije",
        "sp-contributions-newonly": "Prikaži samo izmjene kojima su napravljene nove stranice",
        "sp-contributions-submit": "Traži",
-       "whatlinkshere": "Što vodi ovdje / Шта води овде",
+       "whatlinkshere": "Što upućuje ovamo",
        "whatlinkshere-title": "Stranice koje vode / Странице које воде до $1",
        "whatlinkshere-page": "Stranica:",
        "linkshere": "Sljedeće stranice vode na '''[[:$1]]''':",
        "databasenotlocked": "Baza podataka nije zaključana.",
        "lockedbyandtime": "(od $1 dana $2 u $3)",
        "move-page": "Preusmjeravanje $1",
-       "move-page-legend": "Premjestite stranicu",
+       "move-page-legend": "Premjesti stranicu",
        "movepagetext": "Korištenjem ovog formulara možete preimenovati stranicu, premještajući cijelu historiju na novo ime.\nČlanak pod starim imenom će postati stranica koja preusmjerava na članak pod novim imenom. \nMožete automatski izmjeniti preusmjerenje do izvornog naslova.\nAko se ne odlučite na to, provjerite [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|neispravna preusmjeravanja]].\nDužni ste provjeriti da svi linkovi i dalje nastave voditi na prave stranice.\n\nImajte na umu da članak '''neće''' biti preusmjeren ukoliko već postoji članak pod imenom na koje namjeravate da preusmjerite osim u slučaju stranice za preusmjeravanje koja nema nikakvih starih izmjena.\nTo znači da možete vratiti stranicu na prethodno mjesto ako pogriješite, ali ne možete zamijeniti postojeću stranicu.\n\n'''Pažnja!'''\nOvo može biti drastična i neočekivana promjena kad su u pitanju popularne stranice;\nMolimo dobro razmislite prije nego što preimenujete stranicu.",
        "movepagetext-noredirectfixer": "Koristeći obrazac ispod ćete preimenovati stranicu i premjestiti cijelu njenu historiju na novi naziv.\nStari naziv će postati preusmjerenje na novi naziv.\nMolimo provjerite da li postoje [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|nedovršena preusmjerenja]].\nVi ste za to odgovorni te morate provjeriti da li su linkovi ispravni i da li vode tamo gdje bi trebali.\n\nImajte na umu da stranica '''neće''' biti premještena ako već postoji stranica s tim imenom, osim ako je prazna ili je preusmjerenje ili nema ranije historije.\nOvo znali da možete preimenovati stranicu nazad gdje je ranije bila preimenovana ako ste pogriješili a ne možete ponovo preimenovati postojeću stranicu.\n\n'''Pažnja!'''\nImajte na umu da preusmjeravanje popularnog članka može biti\ndrastična i neočekivana promjena za korisnike; molimo budite sigurni da ste shvatili posljedice prije nego što nastavite.",
        "movepagetalktext": "Ako označite ovu kutijucu, pridružena stranica za razgovor će se automatski premjestiti na novi naslov, ukoliko ne-prazna stranica razgovor sa istim imenom već postoji. U tom slučaju ćete morati, ako želite, ručno premjestiti ili spojiti stranicu.",
        "cant-move-category-page": "Nemate dopuštene da premještate stranice kategorija.",
        "cant-move-to-category-page": "Nemate dopuštenje da premjestite stranicu na stranicu kategorije.",
        "newtitle": "Novi naslov:",
-       "move-watch": "Prati ovu stranicu - Прати ову страницу",
-       "movepagebtn": "Premjesti stranicu – Премјести страницу",
+       "move-watch": "Motri izvornu i ciljnu stranicu",
+       "movepagebtn": "Premjesti stranicu",
        "pagemovedsub": "Premještanje uspjelo",
        "movepage-moved": "'''\"$1\" je premještena na \"$2\"'''",
        "movepage-moved-redirect": "Preusmjerenje je napravljeno.",
        "movepage-moved-noredirect": "Pravljenje preusmjerenja je onemogućeno.",
        "articleexists": "Stranica pod tim imenom već postoji, ili je ime koje ste izabrali neispravno.\nMolimo Vas da izaberete drugo ime.",
        "cantmove-titleprotected": "Ne možete premjestiti stranicu na ovu lokaciju, jer je novi naslov zaštićen od pravljenja",
-       "movetalk": "Premjesti i stranicu za diskusiju zajedno sa člankom (ako nije prazna).",
-       "move-subpages": "Premjesti sve podstranice (do $1)",
-       "move-talk-subpages": "Premjesti podstranice stranica za razgovor (do $1)",
+       "movetalk": "Premjesti pridruženu stranicu za razgovor",
+       "move-subpages": "Premjesti podstranice (sve do $1)",
+       "move-talk-subpages": "Premjesti podstranice stranice za razgovor (sve do $1)",
        "movepage-page-exists": "Stranica $1 već postoji i ne može biti automatski zamijenjena.",
        "movepage-page-moved": "Stranica $1 je premještena na $2.",
        "movepage-page-unmoved": "Stranica $1 ne može biti premještena na $2.",
        "nonfile-cannot-move-to-file": "Ne mogu se premjestiti podaci u datotečni imenski prostor",
        "imagetypemismatch": "Ekstenzija nove datoteke ne odgovara njenom tipu",
        "imageinvalidfilename": "Ciljno ime datoteke nije valjano",
-       "fix-double-redirects": "Ažuriraj sva preusmjerenja koja vode ka originalnom naslovu",
+       "fix-double-redirects": "Ažuriraj sva preusmjerenja koja vode na originalni naslov",
        "move-leave-redirect": "Ostavi preusmjerenje",
        "protectedpagemovewarning": "'''Upozorenje:''' Ova stranica je zaključana tako da je mogu premještati samo korisnici sa ovlastima administratora.\nPosljednja stavka evidencije je prikazana ispod kao referenca:",
        "semiprotectedpagemovewarning": "'''Napomena:''' Ova stranica je zaključana tako da je mogu uređivati samo registrovani korisnici.\nPosljednja stavka evidencije je prikazana ispod kao referenca:",
        "tooltip-pt-login": "Predlažem da se prijavite; međutim, to nije obavezno",
        "tooltip-pt-logout": "Odjava sa projekta {{SITENAME}}",
        "tooltip-pt-createaccount": "Ohrabrujemo vas da otvorite račun i prijavite se; to, međutim, nije obavezno",
-       "tooltip-ca-talk": "Razgovor o sadržaju stranice",
+       "tooltip-ca-talk": "Rasprava o stranici sa sadržajem",
        "tooltip-ca-edit": "Uredi ovu stranicu",
        "tooltip-ca-addsection": "Započnite novu sekciju.",
        "tooltip-ca-viewsource": "Ova stranica je zaštićena.\nMožete vidjeti njen izvor",
        "tooltip-ca-delete": "Izbriši ovu stranicu",
        "tooltip-ca-undelete": "Vratite izmjene koje su načinjene prije brisanja stranice",
        "tooltip-ca-move": "Premjesti ovu stranicu",
-       "tooltip-ca-watch": "Dodajte ovu stranicu na Vaš spisak praćenja",
+       "tooltip-ca-watch": "Dodaj ovu stranicu na svoju listu motrenja",
        "tooltip-ca-unwatch": "Izbrišite ovu stranicu sa spiska praćenja",
        "tooltip-search": "Traži ovaj Wiki / Тражи овај Вики [alt-f]",
        "tooltip-search-go": "Idi na stranicu s upravo ovakvim imenom ako postoji",
        "tooltip-n-currentevents": "Pronađi dodatne informacije o trenutnim događajima",
        "tooltip-n-recentchanges": "Spisak nedavnih izmjena na wikiju.",
        "tooltip-n-randompage": "Otvorite slučajnu stranicu",
-       "tooltip-n-help": "Mjesto gdje možete nešto da naučite",
-       "tooltip-t-whatlinkshere": "Spisak svih stranica povezanih sa ovim",
+       "tooltip-n-help": "Mjesto za saznavanje",
+       "tooltip-t-whatlinkshere": "Lista svih wiki stranica koje upućuju ovamo",
        "tooltip-t-recentchangeslinked": "Nedavne izmjene ovdje povezanih stranica",
        "tooltip-feed-rss": "RSS feed za ovu stranicu",
        "tooltip-feed-atom": "Atom feed za ovu stranicu",
        "tooltip-t-contributions": "Pogledajte listu doprinosa ovog korisnika",
        "tooltip-t-emailuser": "Pošaljite e-mail ovom korisniku",
        "tooltip-t-upload": "Postavi datoteke",
-       "tooltip-t-specialpages": "Popis svih posebnih stranica",
-       "tooltip-t-print": "Verzija ove stranice za štampanje",
+       "tooltip-t-specialpages": "Lista svih posebnih stranica",
+       "tooltip-t-print": "Verzija ove stranice za ispis",
        "tooltip-t-permalink": "Stalni link ove verzije stranice",
-       "tooltip-ca-nstab-main": "Pogledajte sadržaj stranice",
+       "tooltip-ca-nstab-main": "Vidi stranicu sa sadržajem",
        "tooltip-ca-nstab-user": "Pogledajte korisničku stranicu",
        "tooltip-ca-nstab-media": "Pogledajte medijski fajl",
        "tooltip-ca-nstab-special": "Ovo je posebna stranica, te se ne može zasebno uređivati",
        "tooltip-ca-nstab-image": "Vidi stranicu datoteke/fajla",
        "tooltip-ca-nstab-mediawiki": "Pogledajte sistemsku poruku",
        "tooltip-ca-nstab-template": "Pogledajte šablon",
-       "tooltip-ca-nstab-help": "Pogledajte stranicu za pomoć",
+       "tooltip-ca-nstab-help": "Vidi stranicu pomoći",
        "tooltip-ca-nstab-category": "Pogledajte stranicu kategorije",
-       "tooltip-minoredit": "Označite ovo kao manju izmjenu",
-       "tooltip-save": "Snimi izmjene - Сними измјене [alt-s]",
-       "tooltip-preview": "Prethodni pregled stranice, molimo koristiti prije snimanja!",
+       "tooltip-minoredit": "Označi ovo kao manje uređenje",
+       "tooltip-save": "Spremite svoje izmjene",
+       "tooltip-preview": "Pregledajte svoje izmjene. Molimo vas da ovo koristite prije spremanja.",
        "tooltip-diff": "Prikaz izmjena koje ste napravili u tekstu",
        "tooltip-compareselectedversions": "Pogledajte pazlike između dvije selektovane verzije ove stranice.",
-       "tooltip-watch": "Postavite ovu stranicu na Vaš spisak praćenja / Поставите ову страницу на Ваш списак праћења [alt-w]",
+       "tooltip-watch": "Dodaj ovu stranicu na svoju listu motrenja",
        "tooltip-watchlistedit-normal-submit": "Ukloni naslove",
        "tooltip-watchlistedit-raw-submit": "Ažuriraj spisak praćenja",
        "tooltip-recreate": "Ponovno pravljenje stranice iako je već brisana",
        "exif-attributionurl": "Kada ponovno koristite ovaj rad, molimo povežite ga na",
        "exif-preferredattributionname": "Kada ponovno koristite ovaj rad, molimo pripišite ga na",
        "exif-pngfilecomment": "PNG komentar datoteke",
-       "exif-disclaimer": "Odricanje odgovornosti",
+       "exif-disclaimer": "Odricanje od odgovornosti",
        "exif-contentwarning": "Upozorenje o sadržaju",
        "exif-giffilecomment": "GIF komentar datoteke",
        "exif-intellectualgenre": "Tip predmeta",
        "fileduplicatesearch-result-1": "Datoteka \"$1\" nema identičnih dvojnika.",
        "fileduplicatesearch-result-n": "Datoteka \"$1\" ima {{PLURAL:$2|1 identičnog|$2 identična|$2 identičnih}} dvojnika.",
        "fileduplicatesearch-noresults": "Nije pronađena datoteka sa imenom \"$1\".",
-       "specialpages": "Posebno / Посебно",
+       "specialpages": "Posebne stranice",
        "specialpages-note": "* Normalne posebne stranice.\n* <span class=\"mw-specialpagerestricted\">Ograničene posebne stranice.</span>\n* <span class=\"mw-specialpagecached\">Keširane posebne stranice (mogu biti zastarjele).</span>",
        "specialpages-group-maintenance": "Izvještaji o održavanju / Извјештаји о одржавању",
        "specialpages-group-other": "Ostale posebne stranice - Остале посебне странице",
        "feedback-bugcheck": "Izvrsno! Molimo provjerite da se ne radi o nekom [$1 poznatom \"bugu\"].",
        "feedback-bugnew": "Provereno. Prijavi novu grešku",
        "feedback-bugornote": "Ako ste spremni da detaljno opišete tehnički problem, onda [$1 prijavite grešku].\nU suprotnom, poslužite se jednostavnim obrascem ispod. Vaš komentar će stajati na stranici „[$3 $2]“, zajedno s korisničkim imenom i pregledačem koji koristite.",
-       "feedback-cancel": "Odustani",
+       "feedback-cancel": "Otkaži",
        "feedback-close": "Gotovo",
        "feedback-error1": "Greška: neprepoznat rezultat od API-ja",
        "feedback-error2": "Greška: Uređivanje nije uspjelo",
        "duration-centuries": "$1 {{PLURAL:$1|vijek|vijekova}}",
        "duration-millennia": "$1 {{PLURAL:$1|milenijum|milenijuma}}",
        "rotate-comment": "Slika rotirana za $1 {{PLURAL:$1|stepeni}} u smjeru kazaljke na satu",
-       "expand_templates_input": "Unos - Унос"
+       "expand_templates_input": "Unos - Унос",
+       "expand_templates_preview": "Pregled"
 }
index a2a1411..110c281 100644 (file)
        "createacct-reason-ph": "Чому ви створюєте інший обліковий запис",
        "createacct-submit": "Створіть ваш обліковий запис",
        "createacct-another-submit": "Створити обліковий запис",
-       "createacct-benefit-heading": "{{SITENAME}} Ñ\81Ñ\82воÑ\80Ñ\8eÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f Ñ\82акими Ñ\81амими Ð»Ñ\8eдÑ\8cми, Ñ\8fк Ñ\96 Ð²и.",
+       "createacct-benefit-heading": "{{SITENAME}} Ñ\81Ñ\82воÑ\80Ñ\8eÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f Ñ\82акими Ñ\81амими Ð»Ñ\8eдÑ\8cми, Ñ\8fк Ñ\96 Ð\92и.",
        "createacct-benefit-body1": "{{PLURAL:$1|редагування|редагування|редагувань}}",
        "createacct-benefit-body2": "{{PLURAL:$1|сторінка|сторінки|сторінок}}",
        "createacct-benefit-body3": "{{PLURAL:$1|дописувач|дописувачі|дописувачів}} цього місяця",
index 4adf154..fb26675 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Goan Konkani (à¤\97à¥\8bवा à¤\95à¥\8bà¤\82à¤\95णà¥\80 / Gova Konknni)
+/** Goan Konkani (à¤\97à¥\8bà¤\82यà¤\9aà¥\80 à¤\95à¥\8bà¤\82à¤\95णà¥\80 / Gõychi Konknni)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index b5cc343..190bc4d 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Goan Konkani - Devanagari script (à¤\97à¥\8bवा कोंकणी)
+/** Goan Konkani - Devanagari script (à¤\97à¥\8bà¤\82यà¤\9aà¥\80 कोंकणी)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index 85c58d7..f4ef540 100644 (file)
@@ -1,20 +1,19 @@
 @import "mediawiki.mixins";
 
-// Table Sorting
+/* Table Sorting */
 
-.client-js table.jquery-tablesorter th.headerSort {
+table.jquery-tablesorter th.headerSort {
        .background-image-svg( 'images/sort_both.svg', 'images/sort_both.png' );
        cursor: pointer;
-       // Keep synchronized with mediawiki.skinning.content styles
        background-repeat: no-repeat;
        background-position: center right;
        padding-right: 21px;
 }
 
-.client-js table.jquery-tablesorter th.headerSortUp {
+table.jquery-tablesorter th.headerSortUp {
        .background-image-svg( 'images/sort_up.svg', 'images/sort_up.png' );
 }
 
-.client-js table.jquery-tablesorter th.headerSortDown {
+table.jquery-tablesorter th.headerSortDown {
        .background-image-svg( 'images/sort_down.svg', 'images/sort_down.png' );
 }
index a873cdf..c88d00d 100644 (file)
@@ -241,16 +241,3 @@ div.tright {
 div.tleft {
        margin: .5em 1.4em 1.3em 0;
 }
-
-/* Make space for the jquery.tablesorter icon and display a placeholder if JavaScript is loaded, while
-   tablesorter is still loading and setting up the tables for sorting. This avoids a flash of
-   unstyled content during page load (FOUC). The styles can also be used by WYSIWYG editors. */
-.client-js table.sortable th:not(.unsortable) {
-       background-image: url(images/sort_both_readonly.png);
-       /* @embed */
-       background-image: linear-gradient(transparent, transparent), url(images/sort_both_readonly.svg);
-       /* Keep synchronised with jquery.tablesorter styles */
-       background-repeat: no-repeat;
-       background-position: center right;
-       padding-right: 21px;
-}
diff --git a/resources/src/mediawiki.skinning/images/sort_both_readonly.png b/resources/src/mediawiki.skinning/images/sort_both_readonly.png
deleted file mode 100644 (file)
index bdb09e3..0000000
Binary files a/resources/src/mediawiki.skinning/images/sort_both_readonly.png and /dev/null differ
diff --git a/resources/src/mediawiki.skinning/images/sort_both_readonly.svg b/resources/src/mediawiki.skinning/images/sort_both_readonly.svg
deleted file mode 100644 (file)
index 3b97000..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 9" height="9" width="21">
-    <path d="M14.5 5l-4 4-4-4zM14.5 4l-4-4-4 4z" style="fill-opacity: 0.5;"/>
-</svg>
index 4be4a8d..a2386b3 100644 (file)
                $checkboxes.prop( 'checked', check );
        }
 
-       $( '#checkbox-all' ).click( function ( e ) {
+       $( '.mw-checkbox-all' ).click( function ( e ) {
                selectAll( true );
                e.preventDefault();
        } );
-       $( '#checkbox-none' ).click( function ( e ) {
+       $( '.mw-checkbox-none' ).click( function ( e ) {
                selectAll( false );
                e.preventDefault();
        } );
-       $( '#checkbox-invert' ).click( function ( e ) {
+       $( '.mw-checkbox-invert' ).click( function ( e ) {
                $checkboxes.each( function () {
                        $( this ).prop( 'checked', !$( this ).is( ':checked' ) );
                } );
index 90ee1bb..ce9e1d6 100644 (file)
@@ -364,6 +364,25 @@ class EditPageTest extends MediaWikiLangTestCase {
        }
 
        public function testUpdatePage() {
+               $checkIds = array();
+
+               $this->setMwGlobals( 'wgHooks', array(
+                       'PageContentInsertComplete' => array( function (
+                               WikiPage &$page, User &$user, Content $content,
+                               $summary, $minor, $u1, $u2, &$flags, Revision $revision
+                       ) {
+                               // types/refs checked
+                       } ),
+                       'PageContentSaveComplete' => array( function (
+                               WikiPage &$page, User &$user, Content $content,
+                               $summary, $minor, $u1, $u2, &$flags, Revision $revision,
+                               Status &$status, $baseRevId
+                       ) use ( &$checkIds ) {
+                               $checkIds[] = $status->value['revision']->getId();
+                               // types/refs checked
+                       } ),
+               ) );
+
                $text = "one";
                $edit = array(
                        'wpTextbox1' => $text,
@@ -373,6 +392,7 @@ class EditPageTest extends MediaWikiLangTestCase {
                $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
                        EditPage::AS_SUCCESS_UPDATE, $text,
                        "expected successfull update with given text" );
+               $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
 
                $this->forceRevisionDate( $page, '20120101000000' );
 
@@ -385,6 +405,62 @@ class EditPageTest extends MediaWikiLangTestCase {
                $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
                        EditPage::AS_SUCCESS_UPDATE, $text,
                        "expected successfull update with given text" );
+               $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
+               $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
+       }
+
+       public function testUpdatePageTrx() {
+               $text = "one";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'first update',
+               );
+
+               $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $text,
+                       "expected successfull update with given text" );
+
+               $this->forceRevisionDate( $page, '20120101000000' );
+
+               $checkIds = array();
+               $this->setMwGlobals( 'wgHooks', array(
+                       'PageContentSaveComplete' => array( function (
+                               WikiPage &$page, User &$user, Content $content,
+                               $summary, $minor, $u1, $u2, &$flags, Revision $revision,
+                               Status &$status, $baseRevId
+                       ) use ( &$checkIds ) {
+                               $checkIds[] = $status->value['revision']->getId();
+                               // types/refs checked
+                       } ),
+               ) );
+
+               wfGetDB( DB_MASTER )->begin( __METHOD__ );
+
+               $text = "two";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'second update',
+               );
+
+               $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $text,
+                       "expected successfull update with given text" );
+
+               $text = "three";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'third update',
+               );
+
+               $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $text,
+                       "expected successfull update with given text" );
+
+               wfGetDB( DB_MASTER )->commit( __METHOD__ );
+
+               $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
+               $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
+               $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
        }
 
        public static function provideSectionEdit() {
diff --git a/tests/phpunit/includes/MergeHistoryTest.php b/tests/phpunit/includes/MergeHistoryTest.php
new file mode 100644 (file)
index 0000000..0c1a7a8
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @group Database
+ */
+class MergeHistoryTest extends MediaWikiTestCase {
+
+       /**
+        * Make some pages to work with
+        */
+       public function addDBData() {
+               // Pages that won't actually be merged
+               $this->insertPage( 'Test' );
+               $this->insertPage( 'Test2' );
+
+               // Pages that will be merged
+               $this->insertPage( 'Merge1' );
+               $this->insertPage( 'Merge2' );
+       }
+
+       /**
+        * @dataProvider provideIsValidMerge
+        * @covers MergeHistory::isValidMerge
+        * @param $source string Source page
+        * @param $dest string Destination page
+        * @param $timestamp string|bool Timestamp up to which revisions are merged (or false for all)
+        * @param $error string|bool Expected error for test (or true for no error)
+        */
+       public function testIsValidMerge( $source, $dest, $timestamp, $error ) {
+               $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+               $mh = new MergeHistory(
+                       Title::newFromText( $source ),
+                       Title::newFromText( $dest ),
+                       $timestamp
+               );
+               $status = $mh->isValidMerge();
+               if ( $error === true ) {
+                       $this->assertTrue( $status->isGood() );
+               } else {
+                       $this->assertTrue( $status->hasMessage( $error ) );
+               }
+       }
+
+       public static function provideIsValidMerge() {
+               return array(
+                       // for MergeHistory::isValidMerge
+                       array( 'Test', 'Test2', false, true ),
+                       // Although this timestamp is after the latest timestamp of both pages,
+                       // MergeHistory should select the latest source timestamp up to this which should
+                       // still work for the merge.
+                       array( 'Test', 'Test2', strtotime( 'tomorrow' ), true ),
+                       array( 'Test', 'Test', false, 'mergehistory-fail-self-merge' ),
+                       array( 'Nonexistant', 'Test2', false, 'mergehistory-fail-invalid-source' ),
+                       array( 'Test', 'Nonexistant', false, 'mergehistory-fail-invalid-dest' ),
+                       array(
+                               'Test',
+                               'Test2',
+                               'This is obviously an invalid timestamp',
+                               'mergehistory-fail-bad-timestamp'
+                       ),
+               );
+       }
+
+       /**
+        * Test merge revision limit checking
+        * @covers MergeHistory::isValidMerge
+        */
+       public function testIsValidMergeRevisionLimit() {
+               $limit = MergeHistory::REVISION_LIMIT;
+
+               $mh = $this->getMockBuilder( 'MergeHistory' )
+                       ->setMethods( array( 'getRevisionCount' ) )
+                       ->setConstructorArgs( array(
+                               Title::newFromText( 'Test' ),
+                               Title::newFromText( 'Test2' ),
+                       ) )
+                       ->getMock();
+               $mh->expects( $this->once() )
+                       ->method( 'getRevisionCount' )
+                       ->will( $this->returnValue( $limit + 1 ) );
+
+               $status = $mh->isValidMerge();
+               $this->assertTrue( $status->hasMessage( 'mergehistory-fail-toobig' ) );
+               $errors = $status->getErrorsByType( 'error' );
+               $params = $errors[0]['params'];
+               $this->assertEquals( $params[0], Message::numParam( $limit ) );
+       }
+
+       /**
+        * Test user permission checking
+        * @covers MergeHistory::checkPermissions
+        */
+       public function testCheckPermissions() {
+               $mh = new MergeHistory(
+                       Title::newFromText( 'Test' ),
+                       Title::newFromText( 'Test2' )
+               );
+
+               // Sysop with mergehistory permission
+               $sysop = User::newFromName( 'UTSysop' );
+               $status = $mh->checkPermissions( $sysop, '' );
+               $this->assertTrue( $status->isOK() );
+
+               // Normal user
+               $notSysop = User::newFromName( 'UTNotSysop' );
+               $notSysop->addToDatabase();
+               $status = $mh->checkPermissions( $notSysop, '' );
+               $this->assertTrue( $status->hasMessage( 'mergehistory-fail-permission' ) );
+       }
+
+       /**
+        * Test merged revision count
+        * @covers MergeHistory::getMergedRevisionCount
+        */
+       public function testGetMergedRevisionCount() {
+               $mh = new MergeHistory(
+                       Title::newFromText( 'Merge1' ),
+                       Title::newFromText( 'Merge2' )
+               );
+
+               $mh->merge( User::newFromName( 'UTSysop' ) );
+               $this->assertEquals( $mh->getMergedRevisionCount(), 1 );
+       }
+}
index 52872a4..e1ba0ba 100644 (file)
@@ -207,11 +207,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
        }
 
        public function testCheckSessionInfo() {
-               $logger = new \TestLogger( true, function ( $m ) {
-                       return preg_replace(
-                               '/^Session \[\d+\][a-zA-Z0-9_\\\\]+<(?:null|anon|[+-]:\d+:\w+)>\w+: /', 'Session X: ', $m
-                       );
-               } );
+               $logger = new \TestLogger( true );
                $provider = $this->getProvider();
                $provider->setLogger( $logger );
 
@@ -242,7 +238,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
 
                        $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
                        $this->assertSame( array(
-                               array( LogLevel::INFO, "Session X: Missing metadata: $key" )
+                               array( LogLevel::INFO, 'Session "{session}": Missing metadata: {missing}' )
                        ), $logger->getBuffer() );
                        $logger->clearBuffer();
                }
@@ -253,7 +249,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
                $metadata = $info->getProviderMetadata();
                $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
                $this->assertSame( array(
-                       array( LogLevel::INFO, "Session X: No BotPassword for {$bp->getUserCentralId()} Foobar" ),
+                       array( LogLevel::INFO, 'Session "{session}": No BotPassword for {centralId} {appId}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -263,7 +259,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
                $metadata = $info->getProviderMetadata();
                $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'Session X: BotPassword token check failed' ),
+                       array( LogLevel::INFO, 'Session "{session}": BotPassword token check failed' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -275,7 +271,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase {
                $metadata = $info->getProviderMetadata();
                $this->assertFalse( $provider->refreshSessionInfo( $info, $request2, $metadata ) );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'Session X: Restrictions check failed' ),
+                       array( LogLevel::INFO, 'Session "{session}": Restrictions check failed' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
index 9ba67df..f5c8b05 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Session;
 
 use MediaWikiTestCase;
 use User;
+use Psr\Log\LogLevel;
 
 /**
  * @group Session
@@ -159,7 +160,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                        'cookieOptions' => array( 'prefix' => 'x' ),
                );
                $provider = new CookieSessionProvider( $params );
-               $provider->setLogger( new \TestLogger() );
+               $logger = new \TestLogger( true );
+               $provider->setLogger( $logger );
                $provider->setConfig( $this->getConfig() );
                $provider->setManager( new SessionManager() );
 
@@ -174,6 +176,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $request = new \FauxRequest();
                $info = $provider->provideSessionInfo( $request );
                $this->assertNull( $info );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // Session key only
                $request = new \FauxRequest();
@@ -188,6 +192,13 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( 0, $info->getUserInfo()->getId() );
                $this->assertNull( $info->getUserInfo()->getName() );
                $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(
+                       array(
+                               LogLevel::DEBUG,
+                               'Session "{session}" requested without UserID cookie',
+                       ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User, no session key
                $request = new \FauxRequest();
@@ -203,6 +214,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( $id, $info->getUserInfo()->getId() );
                $this->assertSame( $name, $info->getUserInfo()->getName() );
                $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User and session key
                $request = new \FauxRequest();
@@ -219,6 +232,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( $id, $info->getUserInfo()->getId() );
                $this->assertSame( $name, $info->getUserInfo()->getName() );
                $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User with bad token
                $request = new \FauxRequest();
@@ -229,6 +244,13 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                ), '' );
                $info = $provider->provideSessionInfo( $request );
                $this->assertNull( $info );
+               $this->assertSame( array(
+                       array(
+                               LogLevel::WARNING,
+                               'Session "{session}" requested with invalid Token cookie.'
+                       ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User id with no token
                $request = new \FauxRequest();
@@ -245,6 +267,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( $id, $info->getUserInfo()->getId() );
                $this->assertSame( $name, $info->getUserInfo()->getName() );
                $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                $request = new \FauxRequest();
                $request->setCookies( array(
@@ -252,6 +276,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                ), '' );
                $info = $provider->provideSessionInfo( $request );
                $this->assertNull( $info );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User and session key, with forceHTTPS flag
                $request = new \FauxRequest();
@@ -269,6 +295,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( $id, $info->getUserInfo()->getId() );
                $this->assertSame( $name, $info->getUserInfo()->getName() );
                $this->assertTrue( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // Invalid user id
                $request = new \FauxRequest();
@@ -278,6 +306,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                ), '' );
                $info = $provider->provideSessionInfo( $request );
                $this->assertNull( $info );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User id with matching name
                $request = new \FauxRequest();
@@ -295,6 +325,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                $this->assertSame( $id, $info->getUserInfo()->getId() );
                $this->assertSame( $name, $info->getUserInfo()->getName() );
                $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( array(), $logger->getBuffer() );
+               $logger->clearBuffer();
 
                // User id with wrong name
                $request = new \FauxRequest();
@@ -305,6 +337,13 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
                ), '' );
                $info = $provider->provideSessionInfo( $request );
                $this->assertNull( $info );
+               $this->assertSame( array(
+                       array(
+                               LogLevel::WARNING,
+                               'Session "{session}" requested with mismatched UserID and UserName cookies.',
+                       ),
+               ), $logger->getBuffer() );
+               $logger->clearBuffer();
        }
 
        public function testGetVaryCookies() {
index 3044aa7..e071132 100644 (file)
@@ -114,7 +114,8 @@ class PHPSessionHandlerTest extends MediaWikiTestCase {
 
                $store = new TestBagOStuff();
                $logger = new \TestLogger( true, function ( $m ) {
-                       return preg_match( '/^SessionBackend a{32} /', $m ) ? null : $m;
+                       // Discard all log events starting with expected prefix
+                       return preg_match( '/^SessionBackend "\{session\}" /', $m ) ? null : $m;
                } );
                $manager = new SessionManager( array(
                        'store' => $store,
index bb00121..6be8957 100644 (file)
@@ -2,8 +2,9 @@
 
 namespace MediaWiki\Session;
 
-use Psr\Log\LogLevel;
+use AuthPlugin;
 use MediaWikiTestCase;
+use Psr\Log\LogLevel;
 use User;
 
 /**
@@ -763,6 +764,9 @@ class SessionManagerTest extends MediaWikiTestCase {
 
                \ObjectCache::$instances[__METHOD__] = new TestBagOStuff();
                $this->setMwGlobals( array( 'wgMainCacheType' => __METHOD__ ) );
+               $this->setMWGlobals( array(
+                       'wgAuth' => new AuthPlugin,
+               ) );
 
                $this->stashMwGlobals( array( 'wgGroupPermissions' ) );
                $wgGroupPermissions['*']['createaccount'] = true;
@@ -778,7 +782,6 @@ class SessionManagerTest extends MediaWikiTestCase {
                                return null;
                        }
                        $m = str_replace( 'MediaWiki\Session\SessionManager::autoCreateUser: ', '', $m );
-                       $m = preg_replace( '/ - from: .*$/', ' - from: XXX', $m );
                        return $m;
                } );
                $manager->setLogger( $logger );
@@ -804,7 +807,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                        $user->getId(), User::idFromName( 'UTSessionAutoCreate1', User::READ_LATEST )
                );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate1) - from: XXX' ),
+                       array( LogLevel::INFO, 'creating new user ({username}) - from: {url}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -818,7 +821,10 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
                $session->clear();
                $this->assertSame( array(
-                       array( LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting' ),
+                       array(
+                               LogLevel::DEBUG,
+                               'user is blocked from this wiki, blacklisting',
+                       ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -834,7 +840,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                        $user->getId(), User::idFromName( 'UTSessionAutoCreate2', User::READ_LATEST )
                );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate2) - from: XXX' ),
+                       array( LogLevel::INFO, 'creating new user ({username}) - from: {url}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -872,7 +878,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                        $user->getId(), User::idFromName( 'UTSessionAutoCreate3', User::READ_LATEST )
                );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate3) - from: XXX' ),
+                       array( LogLevel::INFO, 'creating new user ({username}) - from: {url}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1038,7 +1044,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                        'LocalUserCreated' => array(),
                ) );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate4) - from: XXX' ),
+                       array( LogLevel::INFO, 'creating new user ({username}) - from: {url}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
        }
@@ -1069,11 +1075,7 @@ class SessionManagerTest extends MediaWikiTestCase {
 
        public function testLoadSessionInfoFromStore() {
                $manager = $this->getManager();
-               $logger = new \TestLogger( true, function ( $m ) {
-                       return preg_replace(
-                               '/^Session \[\d+\]\w+<(?:null|anon|[+-]:\d+:\w+)>\w+: /', 'Session X: ', $m
-                       );
-               } );
+               $logger = new \TestLogger( true );
                $manager->setLogger( $logger );
                $request = new \FauxRequest();
 
@@ -1187,7 +1189,10 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Unverified user provided and no metadata to auth it' )
+                       array(
+                               LogLevel::WARNING,
+                               'Session "{session}": Unverified user provided and no metadata to auth it',
+                       )
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1198,7 +1203,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Null provider and no metadata' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Null provider and no metadata' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1221,7 +1226,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertFalse( $info->isIdSafe(), 'sanity check' );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::INFO, 'Session X: No user provided and provider cannot set user' )
+                       array( LogLevel::INFO, 'Session "{session}": No user provided and provider cannot set user' )
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1229,14 +1234,14 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->store->setRawSession( $id, true );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Bad data' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Bad data' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
                $this->store->setRawSession( $id, array( 'data' => array() ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Bad data structure' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1244,21 +1249,21 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->store->setRawSession( $id, array( 'metadata' => $metadata ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Bad data structure' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
                $this->store->setRawSession( $id, array( 'metadata' => $metadata, 'data' => true ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Bad data structure' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
                $this->store->setRawSession( $id, array( 'metadata' => true, 'data' => array() ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Bad data structure' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1268,7 +1273,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                        $this->store->setRawSession( $id, array( 'metadata' => $tmp, 'data' => array() ) );
                        $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                        $this->assertSame( array(
-                               array( LogLevel::WARNING, 'Session X: Bad metadata' ),
+                               array( LogLevel::WARNING, 'Session "{session}": Bad metadata' ),
                        ), $logger->getBuffer() );
                        $logger->clearBuffer();
                }
@@ -1294,7 +1299,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Wrong provider, Bad !== Mock' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Wrong provider Bad !== Mock' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1306,7 +1311,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Unknown provider, Bad' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Unknown provider Bad' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1329,7 +1334,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::ERROR, 'Session X: Invalid ID' ),
+                       array( LogLevel::ERROR, 'Session "{session}": {exception}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1342,7 +1347,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::ERROR, 'Session X: Invalid user name' ),
+                       array( LogLevel::ERROR, 'Session "{session}": {exception}', ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1357,7 +1362,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: User ID mismatch, 2 !== 1' ),
+                       array( LogLevel::WARNING, 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1372,7 +1377,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: User name mismatch, X !== UTSysop' ),
+                       array( LogLevel::WARNING, 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1388,7 +1393,8 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
                        array(
-                               LogLevel::WARNING, 'Session X: User ID matched but name didn\'t (rename?), X !== UTSysop'
+                               LogLevel::WARNING,
+                               'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}'
                        ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
@@ -1405,7 +1411,9 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
                        array(
-                               LogLevel::WARNING, 'Session X: Metadata has an anonymous user, but a non-anon user was provided'
+                               LogLevel::WARNING,
+                               'Session "{session}": Metadata has an anonymous user, ' .
+                               'but a non-anon user was provided',
                        ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
@@ -1489,7 +1497,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: User token mismatch' ),
+                       array( LogLevel::WARNING, 'Session "{session}": User token mismatch' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1533,7 +1541,10 @@ class SessionManagerTest extends MediaWikiTestCase {
                ) );
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Metadata merge failed: no merge!' ),
+                       array(
+                               LogLevel::WARNING,
+                               'Session "{session}": Metadata merge failed: {exception}',
+                       ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
 
@@ -1664,7 +1675,7 @@ class SessionManagerTest extends MediaWikiTestCase {
                $this->assertFalse( $loadSessionInfoFromStore( $info ) );
                $this->assertTrue( $called );
                $this->assertSame( array(
-                       array( LogLevel::WARNING, 'Session X: Hook aborted' ),
+                       array( LogLevel::WARNING, 'Session "{session}": Hook aborted' ),
                ), $logger->getBuffer() );
                $logger->clearBuffer();
        }