Merge "Add more BacklinkJobUtils comments"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 4 Dec 2015 19:24:54 +0000 (19:24 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 4 Dec 2015 19:24:54 +0000 (19:24 +0000)
123 files changed:
RELEASE-NOTES-1.27
autoload.php
includes/DefaultSettings.php
includes/HttpFunctions.php
includes/Linker.php
includes/MediaWiki.php
includes/OutputPage.php
includes/Preferences.php
includes/PrefixSearch.php
includes/Title.php
includes/User.php [deleted file]
includes/UserArray.php [deleted file]
includes/UserArrayFromResult.php [deleted file]
includes/UserRightsProxy.php [deleted file]
includes/api/ApiExpandTemplates.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiParse.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiQueryUsers.php
includes/api/i18n/de.json
includes/api/i18n/diq.json
includes/api/i18n/en.json
includes/api/i18n/eu.json
includes/api/i18n/fr.json
includes/api/i18n/he.json
includes/api/i18n/mr.json
includes/api/i18n/qqq.json
includes/changes/CategoryMembershipChange.php
includes/db/Database.php
includes/debug/logger/monolog/AvroFormatter.php
includes/deferred/DeferrableUpdate.php [new file with mode: 0644]
includes/deferred/DeferredUpdates.php
includes/deferred/HTMLCacheUpdate.php
includes/deferred/LinksUpdate.php
includes/deferred/MergeableUpdate.php [new file with mode: 0644]
includes/deferred/SearchUpdate.php
includes/deferred/SquidUpdate.php
includes/filerepo/file/LocalFile.php
includes/installer/i18n/el.json
includes/installer/i18n/eu.json
includes/installer/i18n/is.json
includes/installer/i18n/it.json
includes/installer/i18n/mk.json
includes/jobqueue/jobs/CategoryMembershipChangeJob.php [new file with mode: 0644]
includes/jobqueue/jobs/HTMLCacheUpdateJob.php
includes/jobqueue/utils/BacklinkJobUtils.php
includes/libs/objectcache/WANObjectCache.php
includes/page/WikiPage.php
includes/revisiondelete/RevDelFileList.php
includes/search/SearchExactMatchRescorer.php [new file with mode: 0644]
includes/skins/SkinTemplate.php
includes/specials/SpecialChangeContentModel.php
includes/specials/SpecialProtectedpages.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialWatchlist.php
includes/user/CentralIdLookup.php [new file with mode: 0644]
includes/user/LocalIdLookup.php [new file with mode: 0644]
includes/user/User.php [new file with mode: 0644]
includes/user/UserArray.php [new file with mode: 0644]
includes/user/UserArrayFromResult.php [new file with mode: 0644]
includes/user/UserRightsProxy.php [new file with mode: 0644]
languages/i18n/ar.json
languages/i18n/az.json
languages/i18n/be-tarask.json
languages/i18n/bgn.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/eu.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/hr.json
languages/i18n/hsb.json
languages/i18n/ilo.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/la.json
languages/i18n/lb.json
languages/i18n/mk.json
languages/i18n/mr.json
languages/i18n/nan.json
languages/i18n/nap.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/oc.json
languages/i18n/pl.json
languages/i18n/ps.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/ru.json
languages/i18n/sah.json
languages/i18n/sd.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/tt-cyrl.json
languages/i18n/zh-hans.json
resources/Resources.php
resources/ResourcesOOUI.php
resources/src/mediawiki.special/mediawiki.special.watchlist.js [new file with mode: 0644]
resources/src/oojs-ui-local.js [new file with mode: 0644]
tests/phpunit/includes/UserArrayFromResultTest.php [deleted file]
tests/phpunit/includes/UserTest.php [deleted file]
tests/phpunit/includes/deferred/DeferredUpdatesTest.php
tests/phpunit/includes/deferred/LinksUpdateTest.php
tests/phpunit/includes/deferred/SquidUpdateTest.php [new file with mode: 0644]
tests/phpunit/includes/user/CentralIdLookupTest.php [new file with mode: 0644]
tests/phpunit/includes/user/LocalIdLookupTest.php [new file with mode: 0644]
tests/phpunit/includes/user/UserArrayFromResultTest.php [new file with mode: 0644]
tests/phpunit/includes/user/UserTest.php [new file with mode: 0644]

index 262bbbc..8d0853e 100644 (file)
@@ -84,6 +84,9 @@ production.
 * Added MWTimestamp::getTimezoneString() which returns the localized timezone
   string, if available. To localize this string, see the comments of
   $wgLocaltimezone in includes/DefaultSettings.php.
+* Added CentralIdLookup, a service that allows extensions needing a concept of
+  "central" users to get that without having to know about specific central
+  authentication extensions.
 
 === External library changes in 1.27 ===
 ==== Upgraded external libraries ====
index 2844dc7..685849b 100644 (file)
@@ -164,8 +164,8 @@ $wgAutoloadLocalClasses = array(
        'BenchIfSwitch' => __DIR__ . '/maintenance/benchmarks/bench_if_switch.php',
        'BenchStrtrStrReplace' => __DIR__ . '/maintenance/benchmarks/bench_strtr_str_replace.php',
        'BenchUtf8TitleCheck' => __DIR__ . '/maintenance/benchmarks/bench_utf8_title_check.php',
-       'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php',
        'BenchWfIsWindows' => __DIR__ . '/maintenance/benchmarks/bench_wfIsWindows.php',
+       'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php',
        'BenchmarkDeleteTruncate' => __DIR__ . '/maintenance/benchmarks/bench_delete_truncate.php',
        'BenchmarkHooks' => __DIR__ . '/maintenance/benchmarks/benchmarkHooks.php',
        'BenchmarkParse' => __DIR__ . '/maintenance/benchmarks/benchmarkParse.php',
@@ -192,12 +192,14 @@ $wgAutoloadLocalClasses = array(
        'Category' => __DIR__ . '/includes/Category.php',
        'CategoryFinder' => __DIR__ . '/includes/CategoryFinder.php',
        'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php',
+       'CategoryMembershipChangeJob' => __DIR__ . '/includes/jobqueue/jobs/CategoryMembershipChangeJob.php',
        'CategoryPage' => __DIR__ . '/includes/page/CategoryPage.php',
        'CategoryPager' => __DIR__ . '/includes/specials/SpecialCategories.php',
        'CategoryViewer' => __DIR__ . '/includes/CategoryViewer.php',
        'CdbException' => __DIR__ . '/includes/compat/CdbCompat.php',
        'CdbReader' => __DIR__ . '/includes/compat/CdbCompat.php',
        'CdbWriter' => __DIR__ . '/includes/compat/CdbCompat.php',
+       'CentralIdLookup' => __DIR__ . '/includes/user/CentralIdLookup.php',
        'CgzCopyTransaction' => __DIR__ . '/maintenance/storage/recompressTracked.php',
        'ChangePassword' => __DIR__ . '/maintenance/changePassword.php',
        'ChangeTags' => __DIR__ . '/includes/changetags/ChangeTags.php',
@@ -305,7 +307,7 @@ $wgAutoloadLocalClasses = array(
        'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php',
        'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php',
        'DeadendPagesPage' => __DIR__ . '/includes/specials/SpecialDeadendpages.php',
-       'DeferrableUpdate' => __DIR__ . '/includes/deferred/DeferredUpdates.php',
+       'DeferrableUpdate' => __DIR__ . '/includes/deferred/DeferrableUpdate.php',
        'DeferredStringifier' => __DIR__ . '/includes/libs/DeferredStringifier.php',
        'DeferredUpdates' => __DIR__ . '/includes/deferred/DeferredUpdates.php',
        'DeleteAction' => __DIR__ . '/includes/actions/DeleteAction.php',
@@ -693,6 +695,7 @@ $wgAutoloadLocalClasses = array(
        'LocalFileDeleteBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
        'LocalFileMoveBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
        'LocalFileRestoreBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
+       'LocalIdLookup' => __DIR__ . '/includes/user/LocalIdLookup.php',
        'LocalRepo' => __DIR__ . '/includes/filerepo/LocalRepo.php',
        'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php',
        'LocalisationCache' => __DIR__ . '/includes/cache/LocalisationCache.php',
@@ -790,6 +793,7 @@ $wgAutoloadLocalClasses = array(
        'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
        'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php',
+       'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php',
        'Message' => __DIR__ . '/includes/Message.php',
        'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php',
        'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php',
@@ -1086,6 +1090,7 @@ $wgAutoloadLocalClasses = array(
        'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php',
        'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php',
        'SearchEngineDummy' => __DIR__ . '/includes/search/SearchEngine.php',
+       'SearchExactMatchRescorer' => __DIR__ . '/includes/search/SearchExactMatchRescorer.php',
        'SearchHighlighter' => __DIR__ . '/includes/search/SearchHighlighter.php',
        'SearchMssql' => __DIR__ . '/includes/search/SearchMssql.php',
        'SearchMySQL' => __DIR__ . '/includes/search/SearchMySQL.php',
@@ -1320,9 +1325,9 @@ $wgAutoloadLocalClasses = array(
        'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php',
        'UppercaseCollation' => __DIR__ . '/includes/Collation.php',
        'UsageException' => __DIR__ . '/includes/api/ApiMain.php',
-       'User' => __DIR__ . '/includes/User.php',
-       'UserArray' => __DIR__ . '/includes/UserArray.php',
-       'UserArrayFromResult' => __DIR__ . '/includes/UserArrayFromResult.php',
+       'User' => __DIR__ . '/includes/user/User.php',
+       'UserArray' => __DIR__ . '/includes/user/UserArray.php',
+       'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php',
        'UserBlockedError' => __DIR__ . '/includes/exception/UserBlockedError.php',
        'UserCache' => __DIR__ . '/includes/cache/UserCache.php',
        'UserDupes' => __DIR__ . '/maintenance/userDupes.inc',
@@ -1330,7 +1335,7 @@ $wgAutoloadLocalClasses = array(
        'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php',
        'UserOptions' => __DIR__ . '/maintenance/userOptions.inc',
        'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php',
-       'UserRightsProxy' => __DIR__ . '/includes/UserRightsProxy.php',
+       'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php',
        'UsercreateTemplate' => __DIR__ . '/includes/templates/Usercreate.php',
        'UserloginTemplate' => __DIR__ . '/includes/templates/Userlogin.php',
        'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php',
index 9730265..9d93797 100644 (file)
@@ -4333,6 +4333,21 @@ $wgActiveUserDays = 30;
  * @{
  */
 
+/**
+ * Central ID lookup providers
+ * Key is the provider ID, value is a specification for ObjectFactory
+ * @since 1.27
+ */
+$wgCentralIdLookupProviders = array(
+       'local' => array( 'class' => 'LocalIdLookup' ),
+);
+
+/**
+ * Central ID lookup provider to use by default
+ * @var string
+ */
+$wgCentralIdLookupProvider = 'local';
+
 /**
  * Password policy for local wiki users. A user's effective policy
  * is the superset of all policy statements from the policies for the
@@ -4575,6 +4590,7 @@ $wgDefaultUserOptions = array(
        'watchlisthideown' => 0,
        'watchlisthidepatrolled' => 0,
        'watchlisthidecategorization' => 1,
+       'watchlistreloadautomatically' => 0,
        'watchmoves' => 0,
        'watchrollback' => 0,
        'wllimit' => 250,
@@ -6179,7 +6195,14 @@ $wgRCEngines = array(
 );
 
 /**
- * Treat category membership changes as a RecentChange
+ * Treat category membership changes as a RecentChange.
+ * Changes are mentioned in RC for page actions as follows:
+ *   - creation: pages created with categories are mentioned
+ *   - edit: category additions/removals to existing pages are mentioned
+ *   - move: nothing is mentioned (unless templates used depend on the title)
+ *   - deletion: nothing is mentioned
+ *   - undeletion: nothing is mentioned
+ *
  * @since 1.27
  */
 $wgRCWatchCategoryMembership = false;
@@ -6735,6 +6758,7 @@ $wgJobClasses = array(
        'refreshLinksPrioritized' => 'RefreshLinksJob', // for cascading protection
        'refreshLinksDynamic' => 'RefreshLinksJob', // for pages with dynamic content
        'activityUpdateJob' => 'ActivityUpdateJob',
+       'categoryMembershipChange' => 'CategoryMembershipChangeJob',
        'enqueue' => 'EnqueueJob', // local queue for multi-DC setups
        'null' => 'NullJob'
 );
index 60196ab..e6801e3 100644 (file)
@@ -80,7 +80,8 @@ class Http {
                } else {
                        $errors = $status->getErrorsByType( 'error' );
                        $logger = LoggerFactory::getInstance( 'http' );
-                       $logger->warning( $status->getWikiText(), array( 'caller' => $caller ) );
+                       $logger->warning( $status->getWikiText(),
+                               array( 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ) );
                        return false;
                }
        }
index 842d276..5255b9a 100644 (file)
@@ -1393,7 +1393,10 @@ class Linker {
         * is ignored
         *
         * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
-        * @param string $comment Text to format links in
+        * @param string $comment Text to format links in. WARNING! Since the output of this
+        *      function is html, $comment must be sanitized for use as html. You probably want
+        *      to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
+        *      this function.
         * @param Title|null $title An optional title object used to links to sections
         * @param bool $local Whether section links should refer to local page
         * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
index 38d9e47..ecfd8f8 100644 (file)
@@ -509,8 +509,10 @@ class MediaWiki {
                $factory = wfGetLBFactory();
                $factory->commitMasterChanges();
                $factory->shutdown();
+               wfDebug( __METHOD__ . ': all transactions committed' );
 
-               wfDebug( __METHOD__ . ' completed; all transactions committed' );
+               DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
+               wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
 
                // Set a cookie to tell all CDN edge nodes to "stick" the user to the
                // DC that handles this POST request (e.g. the "master" data center)
index c35204d..00143c7 100644 (file)
@@ -1354,6 +1354,8 @@ class OutputPage extends ContextSource {
                        array( &$this, $categories, &$this->mCategoryLinks ) )
                ) {
                        foreach ( $categories as $category => $type ) {
+                               // array keys will cast numeric category names to ints, so cast back to string
+                               $category = (string)$category;
                                $origcategory = $category;
                                $title = Title::makeTitleSafe( NS_CATEGORY, $category );
                                if ( !$title ) {
index 096f8e3..c7ab9cd 100644 (file)
@@ -1006,6 +1006,11 @@ class Preferences {
                        'section' => 'watchlist/advancedwatchlist',
                        'label-message' => 'tog-watchlisthideliu',
                );
+               $defaultPreferences['watchlistreloadautomatically'] = array(
+                       'type' => 'toggle',
+                       'section' => 'watchlist/advancedwatchlist',
+                       'label-message' => 'tog-watchlistreloadautomatically',
+               );
 
                if ( $config->get( 'RCWatchCategoryMembership' ) ) {
                        $defaultPreferences['watchlisthidecategorization'] = array(
index e328e9f..c6f187d 100644 (file)
@@ -164,112 +164,9 @@ abstract class PrefixSearch {
                return $this->strings( $this->handleResultFromHook( $srchres, $namespaces, $search, $limit ) );
        }
 
-       /**
-        * Default search backend does proper prefix searching, but custom backends
-        * may sort based on other algorythms that may cause the exact title match
-        * to not be in the results or be lower down the list.
-        * @param array $srchres results from the hook
-        * @return array munged results from the hook
-        */
        private function handleResultFromHook( $srchres, $namespaces, $search, $limit ) {
-               // Pick namespace (based on PrefixSearch::defaultSearchBackend)
-               $ns = in_array( NS_MAIN, $namespaces ) ? NS_MAIN : $namespaces[0];
-               $t = Title::newFromText( $search, $ns );
-               if ( !$t || !$t->exists() ) {
-                       // No exact match so just return the search results
-                       return $srchres;
-               }
-               $string = $t->getPrefixedText();
-               $key = array_search( $string, $srchres );
-               if ( $key !== false ) {
-                       // Exact match was in the results so just move it to the front
-                       return $this->pullFront( $key, $srchres );
-               }
-               // Exact match not in the search results so check for some redirect handling cases
-               if ( $t->isRedirect() ) {
-                       $target = $this->getRedirectTarget( $t );
-                       $key = array_search( $target, $srchres );
-                       if ( $key !== false ) {
-                               // Exact match is a redirect to one of the returned matches so pull the
-                               // returned match to the front.  This might look odd but the alternative
-                               // is to put the redirect in front and drop the match.  The name of the
-                               // found match is often more descriptive/better formed than the name of
-                               // the redirect AND by definition they share a prefix.  Hopefully this
-                               // choice is less confusing and more helpful.  But it might not be.  But
-                               // it is the choice we're going with for now.
-                               return $this->pullFront( $key, $srchres );
-                       }
-                       $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
-                       if ( isset( $redirectTargetsToRedirect[$target] ) ) {
-                               // The exact match and something in the results list are both redirects
-                               // to the same thing!  In this case we'll pull the returned match to the
-                               // top following the same logic above.  Again, it might not be a perfect
-                               // choice but it'll do.
-                               return $this->pullFront( $redirectTargetsToRedirect[$target], $srchres );
-                       }
-               } else {
-                       $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
-                       if ( isset( $redirectTargetsToRedirect[$string] ) ) {
-                               // The exact match is the target of a redirect already in the results list so remove
-                               // the redirect from the results list and push the exact match to the front
-                               array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 );
-                               array_unshift( $srchres, $string );
-                               return $srchres;
-                       }
-               }
-
-               // Exact match is totally unique from the other results so just add it to the front
-               array_unshift( $srchres, $string );
-               // And roll one off the end if the results are too long
-               if ( count( $srchres ) > $limit ) {
-                       array_pop( $srchres );
-               }
-               return $srchres;
-       }
-
-       /**
-        * @param Array(string) $titles as strings
-        * @return Array(string => int) redirect target prefixedText to index of title in titles
-        *   that is a redirect to it.
-        */
-       private function redirectTargetsToRedirect( $titles ) {
-               $result = array();
-               foreach ( $titles as $key => $titleText ) {
-                       $title = Title::newFromText( $titleText );
-                       if ( !$title || !$title->isRedirect() ) {
-                               continue;
-                       }
-                       $target = $this->getRedirectTarget( $title );
-                       if ( !$target ) {
-                               continue;
-                       }
-                       $result[$target] = $key;
-               }
-               return $result;
-       }
-
-       /**
-        * @param int $key key to pull to the front
-        * @return array $array with the item at $key pulled to the front
-        */
-       private function pullFront( $key, $array ) {
-               $cut = array_splice( $array, $key, 1 );
-               array_unshift( $array, $cut[0] );
-               return $array;
-       }
-
-       /**
-        * Get a redirect's destination from a title
-        * @param Title $title A title to redirect. It may not redirect or even exist
-        * @return null|string If title exists and redirects, get the destination's prefixed name
-        */
-       private function getRedirectTarget( $title ) {
-               $page = WikiPage::factory( $title );
-               if ( !$page->exists() ) {
-                       return null;
-               }
-               $redir = $page->getRedirectTarget();
-               return $redir ? $redir->getPrefixedText() : null;
+               $rescorer = new SearchExactMatchRescorer();
+               return $rescorer->rescore( $search, $namespaces, $srchres, $limit );
        }
 
        /**
index 4b39efd..46131c1 100644 (file)
@@ -3591,12 +3591,10 @@ class Title {
         * Purge all applicable Squid URLs
         */
        public function purgeSquid() {
-               global $wgUseSquid;
-               if ( $wgUseSquid ) {
-                       $urls = $this->getSquidURLs();
-                       $u = new SquidUpdate( $urls );
-                       $u->doUpdate();
-               }
+               DeferredUpdates::addUpdate(
+                       new SquidUpdate( $this->getSquidURLs() ),
+                       DeferredUpdates::PRESEND
+               );
        }
 
        /**
diff --git a/includes/User.php b/includes/User.php
deleted file mode 100644 (file)
index 3d1aa7e..0000000
+++ /dev/null
@@ -1,5334 +0,0 @@
-<?php
-/**
- * Implements the User class for the %MediaWiki software.
- *
- * 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
- */
-
-/**
- * String Some punctuation to prevent editing from broken text-mangling proxies.
- * @ingroup Constants
- */
-define( 'EDIT_TOKEN_SUFFIX', '+\\' );
-
-/**
- * The User object encapsulates all of the user-specific settings (user_id,
- * name, rights, email address, options, last login time). Client
- * classes use the getXXX() functions to access these fields. These functions
- * do all the work of determining whether the user is logged in,
- * whether the requested option can be satisfied from cookies or
- * whether a database query is needed. Most of the settings needed
- * for rendering normal pages are set in the cookie to minimize use
- * of the database.
- */
-class User implements IDBAccessObject {
-       /**
-        * @const int Number of characters in user_token field.
-        */
-       const TOKEN_LENGTH = 32;
-
-       /**
-        * Global constant made accessible as class constants so that autoloader
-        * magic can be used.
-        */
-       const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
-
-       /**
-        * @const int Serialized record version.
-        */
-       const VERSION = 10;
-
-       /**
-        * Maximum items in $mWatchedItems
-        */
-       const MAX_WATCHED_ITEMS_CACHE = 100;
-
-       /**
-        * Exclude user options that are set to their default value.
-        * @since 1.25
-        */
-       const GETOPTIONS_EXCLUDE_DEFAULTS = 1;
-
-       /**
-        * Array of Strings List of member variables which are saved to the
-        * shared cache (memcached). Any operation which changes the
-        * corresponding database fields must call a cache-clearing function.
-        * @showinitializer
-        */
-       protected static $mCacheVars = array(
-               // user table
-               'mId',
-               'mName',
-               'mRealName',
-               'mEmail',
-               'mTouched',
-               'mToken',
-               'mEmailAuthenticated',
-               'mEmailToken',
-               'mEmailTokenExpires',
-               'mRegistration',
-               'mEditCount',
-               // user_groups table
-               'mGroups',
-               // user_properties table
-               'mOptionOverrides',
-       );
-
-       /**
-        * Array of Strings Core rights.
-        * Each of these should have a corresponding message of the form
-        * "right-$right".
-        * @showinitializer
-        */
-       protected static $mCoreRights = array(
-               'apihighlimits',
-               'applychangetags',
-               'autoconfirmed',
-               'autopatrol',
-               'bigdelete',
-               'block',
-               'blockemail',
-               'bot',
-               'browsearchive',
-               'changetags',
-               'createaccount',
-               'createpage',
-               'createtalk',
-               'delete',
-               'deletedhistory',
-               'deletedtext',
-               'deletelogentry',
-               'deleterevision',
-               'edit',
-               'editcontentmodel',
-               'editinterface',
-               'editprotected',
-               'editmyoptions',
-               'editmyprivateinfo',
-               'editmyusercss',
-               'editmyuserjs',
-               'editmywatchlist',
-               'editsemiprotected',
-               'editusercssjs', # deprecated
-               'editusercss',
-               'edituserjs',
-               'hideuser',
-               'import',
-               'importupload',
-               'ipblock-exempt',
-               'managechangetags',
-               'markbotedits',
-               'mergehistory',
-               'minoredit',
-               'move',
-               'movefile',
-               'move-categorypages',
-               'move-rootuserpages',
-               'move-subpages',
-               'nominornewtalk',
-               'noratelimit',
-               'override-export-depth',
-               'pagelang',
-               'passwordreset',
-               'patrol',
-               'patrolmarks',
-               'protect',
-               'proxyunbannable',
-               'purge',
-               'read',
-               'reupload',
-               'reupload-own',
-               'reupload-shared',
-               'rollback',
-               'sendemail',
-               'siteadmin',
-               'suppressionlog',
-               'suppressredirect',
-               'suppressrevision',
-               'unblockself',
-               'undelete',
-               'unwatchedpages',
-               'upload',
-               'upload_by_url',
-               'userrights',
-               'userrights-interwiki',
-               'viewmyprivateinfo',
-               'viewmywatchlist',
-               'viewsuppressed',
-               'writeapi',
-       );
-
-       /**
-        * String Cached results of getAllRights()
-        */
-       protected static $mAllRights = false;
-
-       /** Cache variables */
-       // @{
-       public $mId;
-       /** @var string */
-       public $mName;
-       /** @var string */
-       public $mRealName;
-
-       /** @var string */
-       public $mEmail;
-       /** @var string TS_MW timestamp from the DB */
-       public $mTouched;
-       /** @var string TS_MW timestamp from cache */
-       protected $mQuickTouched;
-       /** @var string */
-       protected $mToken;
-       /** @var string */
-       public $mEmailAuthenticated;
-       /** @var string */
-       protected $mEmailToken;
-       /** @var string */
-       protected $mEmailTokenExpires;
-       /** @var string */
-       protected $mRegistration;
-       /** @var int */
-       protected $mEditCount;
-       /** @var array */
-       public $mGroups;
-       /** @var array */
-       protected $mOptionOverrides;
-       // @}
-
-       /**
-        * Bool Whether the cache variables have been loaded.
-        */
-       // @{
-       public $mOptionsLoaded;
-
-       /**
-        * Array with already loaded items or true if all items have been loaded.
-        */
-       protected $mLoadedItems = array();
-       // @}
-
-       /**
-        * String Initialization data source if mLoadedItems!==true. May be one of:
-        *  - 'defaults'   anonymous user initialised from class defaults
-        *  - 'name'       initialise from mName
-        *  - 'id'         initialise from mId
-        *  - 'session'    log in from cookies or session if possible
-        *
-        * Use the User::newFrom*() family of functions to set this.
-        */
-       public $mFrom;
-
-       /**
-        * Lazy-initialized variables, invalidated with clearInstanceCache
-        */
-       protected $mNewtalk;
-       /** @var string */
-       protected $mDatePreference;
-       /** @var string */
-       public $mBlockedby;
-       /** @var string */
-       protected $mHash;
-       /** @var array */
-       public $mRights;
-       /** @var string */
-       protected $mBlockreason;
-       /** @var array */
-       protected $mEffectiveGroups;
-       /** @var array */
-       protected $mImplicitGroups;
-       /** @var array */
-       protected $mFormerGroups;
-       /** @var bool */
-       protected $mBlockedGlobally;
-       /** @var bool */
-       protected $mLocked;
-       /** @var bool */
-       public $mHideName;
-       /** @var array */
-       public $mOptions;
-
-       /**
-        * @var WebRequest
-        */
-       private $mRequest;
-
-       /** @var Block */
-       public $mBlock;
-
-       /** @var bool */
-       protected $mAllowUsertalk;
-
-       /** @var Block */
-       private $mBlockedFromCreateAccount = false;
-
-       /** @var array */
-       private $mWatchedItems = array();
-
-       /** @var integer User::READ_* constant bitfield used to load data */
-       protected $queryFlagsUsed = self::READ_NORMAL;
-
-       public static $idCacheByName = array();
-
-       /**
-        * Lightweight constructor for an anonymous user.
-        * Use the User::newFrom* factory functions for other kinds of users.
-        *
-        * @see newFromName()
-        * @see newFromId()
-        * @see newFromConfirmationCode()
-        * @see newFromSession()
-        * @see newFromRow()
-        */
-       public function __construct() {
-               $this->clearInstanceCache( 'defaults' );
-       }
-
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return $this->getName();
-       }
-
-       /**
-        * Load the user table data for this object from the source given by mFrom.
-        *
-        * @param integer $flags User::READ_* constant bitfield
-        */
-       public function load( $flags = self::READ_NORMAL ) {
-               if ( $this->mLoadedItems === true ) {
-                       return;
-               }
-
-               // Set it now to avoid infinite recursion in accessors
-               $this->mLoadedItems = true;
-               $this->queryFlagsUsed = $flags;
-
-               switch ( $this->mFrom ) {
-                       case 'defaults':
-                               $this->loadDefaults();
-                               break;
-                       case 'name':
-                               // Make sure this thread sees its own changes
-                               if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
-                                       $flags |= self::READ_LATEST;
-                                       $this->queryFlagsUsed = $flags;
-                               }
-
-                               $this->mId = self::idFromName( $this->mName, $flags );
-                               if ( !$this->mId ) {
-                                       // Nonexistent user placeholder object
-                                       $this->loadDefaults( $this->mName );
-                               } else {
-                                       $this->loadFromId( $flags );
-                               }
-                               break;
-                       case 'id':
-                               $this->loadFromId( $flags );
-                               break;
-                       case 'session':
-                               if ( !$this->loadFromSession() ) {
-                                       // Loading from session failed. Load defaults.
-                                       $this->loadDefaults();
-                               }
-                               Hooks::run( 'UserLoadAfterLoadFromSession', array( $this ) );
-                               break;
-                       default:
-                               throw new UnexpectedValueException(
-                                       "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
-               }
-       }
-
-       /**
-        * Load user table data, given mId has already been set.
-        * @param integer $flags User::READ_* constant bitfield
-        * @return bool False if the ID does not exist, true otherwise
-        */
-       public function loadFromId( $flags = self::READ_NORMAL ) {
-               if ( $this->mId == 0 ) {
-                       $this->loadDefaults();
-                       return false;
-               }
-
-               // Try cache (unless this needs data from the master DB).
-               // NOTE: if this thread called saveSettings(), the cache was cleared.
-               $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
-               if ( $latest || !$this->loadFromCache() ) {
-                       wfDebug( "User: cache miss for user {$this->mId}\n" );
-                       // Load from DB (make sure this thread sees its own changes)
-                       if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
-                               $flags |= self::READ_LATEST;
-                       }
-                       if ( !$this->loadFromDatabase( $flags ) ) {
-                               // Can't load from ID, user is anonymous
-                               return false;
-                       }
-                       $this->saveToCache();
-               }
-
-               $this->mLoadedItems = true;
-               $this->queryFlagsUsed = $flags;
-
-               return true;
-       }
-
-       /**
-        * @since 1.27
-        * @param string $wikiId
-        * @param integer $userId
-        */
-       public static function purge( $wikiId, $userId ) {
-               $cache = ObjectCache::getMainWANInstance();
-               $cache->delete( $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId ) );
-       }
-
-       /**
-        * @since 1.27
-        * @param WANObjectCache $cache
-        * @return string
-        */
-       protected function getCacheKey( WANObjectCache $cache ) {
-               return $cache->makeGlobalKey( 'user', 'id', wfWikiID(), $this->mId );
-       }
-
-       /**
-        * Load user data from shared cache, given mId has already been set.
-        *
-        * @return bool false if the ID does not exist or data is invalid, true otherwise
-        * @since 1.25
-        */
-       protected function loadFromCache() {
-               if ( $this->mId == 0 ) {
-                       $this->loadDefaults();
-                       return false;
-               }
-
-               $cache = ObjectCache::getMainWANInstance();
-               $data = $cache->get( $this->getCacheKey( $cache ) );
-               if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) {
-                       // Object is expired
-                       return false;
-               }
-
-               wfDebug( "User: got user {$this->mId} from cache\n" );
-
-               // Restore from cache
-               foreach ( self::$mCacheVars as $name ) {
-                       $this->$name = $data[$name];
-               }
-
-               return true;
-       }
-
-       /**
-        * Save user data to the shared cache
-        *
-        * This method should not be called outside the User class
-        */
-       public function saveToCache() {
-               $this->load();
-               $this->loadGroups();
-               $this->loadOptions();
-
-               if ( $this->isAnon() ) {
-                       // Anonymous users are uncached
-                       return;
-               }
-
-               $data = array();
-               foreach ( self::$mCacheVars as $name ) {
-                       $data[$name] = $this->$name;
-               }
-               $data['mVersion'] = self::VERSION;
-               $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
-
-               $cache = ObjectCache::getMainWANInstance();
-               $key = $this->getCacheKey( $cache );
-               $cache->set( $key, $data, $cache::TTL_HOUR, $opts );
-       }
-
-       /** @name newFrom*() static factory methods */
-       // @{
-
-       /**
-        * Static factory method for creation from username.
-        *
-        * This is slightly less efficient than newFromId(), so use newFromId() if
-        * you have both an ID and a name handy.
-        *
-        * @param string $name Username, validated by Title::newFromText()
-        * @param string|bool $validate Validate username. Takes the same parameters as
-        *  User::getCanonicalName(), except that true is accepted as an alias
-        *  for 'valid', for BC.
-        *
-        * @return User|bool User object, or false if the username is invalid
-        *  (e.g. if it contains illegal characters or is an IP address). If the
-        *  username is not present in the database, the result will be a user object
-        *  with a name, zero user ID and default settings.
-        */
-       public static function newFromName( $name, $validate = 'valid' ) {
-               if ( $validate === true ) {
-                       $validate = 'valid';
-               }
-               $name = self::getCanonicalName( $name, $validate );
-               if ( $name === false ) {
-                       return false;
-               } else {
-                       // Create unloaded user object
-                       $u = new User;
-                       $u->mName = $name;
-                       $u->mFrom = 'name';
-                       $u->setItemLoaded( 'name' );
-                       return $u;
-               }
-       }
-
-       /**
-        * Static factory method for creation from a given user ID.
-        *
-        * @param int $id Valid user ID
-        * @return User The corresponding User object
-        */
-       public static function newFromId( $id ) {
-               $u = new User;
-               $u->mId = $id;
-               $u->mFrom = 'id';
-               $u->setItemLoaded( 'id' );
-               return $u;
-       }
-
-       /**
-        * Factory method to fetch whichever user has a given email confirmation code.
-        * This code is generated when an account is created or its e-mail address
-        * has changed.
-        *
-        * If the code is invalid or has expired, returns NULL.
-        *
-        * @param string $code Confirmation code
-        * @param int $flags User::READ_* bitfield
-        * @return User|null
-        */
-       public static function newFromConfirmationCode( $code, $flags = 0 ) {
-               $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-
-               $id = $db->selectField(
-                       'user',
-                       'user_id',
-                       array(
-                               'user_email_token' => md5( $code ),
-                               'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
-                       )
-               );
-
-               return $id ? User::newFromId( $id ) : null;
-       }
-
-       /**
-        * Create a new user object using data from session or cookies. If the
-        * login credentials are invalid, the result is an anonymous user.
-        *
-        * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
-        * @return User
-        */
-       public static function newFromSession( WebRequest $request = null ) {
-               $user = new User;
-               $user->mFrom = 'session';
-               $user->mRequest = $request;
-               return $user;
-       }
-
-       /**
-        * Create a new user object from a user row.
-        * The row should have the following fields from the user table in it:
-        * - either user_name or user_id to load further data if needed (or both)
-        * - user_real_name
-        * - all other fields (email, etc.)
-        * It is useless to provide the remaining fields if either user_id,
-        * user_name and user_real_name are not provided because the whole row
-        * will be loaded once more from the database when accessing them.
-        *
-        * @param stdClass $row A row from the user table
-        * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
-        * @return User
-        */
-       public static function newFromRow( $row, $data = null ) {
-               $user = new User;
-               $user->loadFromRow( $row, $data );
-               return $user;
-       }
-
-       /**
-        * Static factory method for creation of a "system" user from username.
-        *
-        * A "system" user is an account that's used to attribute logged actions
-        * taken by MediaWiki itself, as opposed to a bot or human user. Examples
-        * might include the 'Maintenance script' or 'Conversion script' accounts
-        * used by various scripts in the maintenance/ directory or accounts such
-        * as 'MediaWiki message delivery' used by the MassMessage extension.
-        *
-        * This can optionally create the user if it doesn't exist, and "steal" the
-        * account if it does exist.
-        *
-        * @param string $name Username
-        * @param array $options Options are:
-        *  - validate: As for User::getCanonicalName(), default 'valid'
-        *  - create: Whether to create the user if it doesn't already exist, default true
-        *  - steal: Whether to reset the account's password and email if it
-        *    already exists, default false
-        * @return User|null
-        */
-       public static function newSystemUser( $name, $options = array() ) {
-               $options += array(
-                       'validate' => 'valid',
-                       'create' => true,
-                       'steal' => false,
-               );
-
-               $name = self::getCanonicalName( $name, $options['validate'] );
-               if ( $name === false ) {
-                       return null;
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               $row = $dbw->selectRow(
-                       'user',
-                       array_merge(
-                               self::selectFields(),
-                               array( 'user_password', 'user_newpassword' )
-                       ),
-                       array( 'user_name' => $name ),
-                       __METHOD__
-               );
-               if ( !$row ) {
-                       // No user. Create it?
-                       return $options['create'] ? self::createNew( $name ) : null;
-               }
-               $user = self::newFromRow( $row );
-
-               // A user is considered to exist as a non-system user if it has a
-               // password set, or a temporary password set, or an email set.
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               try {
-                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $password = PasswordFactory::newInvalidPassword();
-               }
-               try {
-                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newpassword = PasswordFactory::newInvalidPassword();
-               }
-               if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
-                       || $user->mEmail
-               ) {
-                       // User exists. Steal it?
-                       if ( !$options['steal'] ) {
-                               return null;
-                       }
-
-                       $nopass = PasswordFactory::newInvalidPassword()->toString();
-
-                       $dbw->update(
-                               'user',
-                               array(
-                                       'user_password' => $nopass,
-                                       'user_newpassword' => $nopass,
-                                       'user_newpass_time' => null,
-                               ),
-                               array( 'user_id' => $user->getId() ),
-                               __METHOD__
-                       );
-                       $user->invalidateEmail();
-                       $user->saveSettings();
-               }
-
-               return $user;
-       }
-
-       // @}
-
-       /**
-        * Get the username corresponding to a given user ID
-        * @param int $id User ID
-        * @return string|bool The corresponding username
-        */
-       public static function whoIs( $id ) {
-               return UserCache::singleton()->getProp( $id, 'name' );
-       }
-
-       /**
-        * Get the real name of a user given their user ID
-        *
-        * @param int $id User ID
-        * @return string|bool The corresponding user's real name
-        */
-       public static function whoIsReal( $id ) {
-               return UserCache::singleton()->getProp( $id, 'real_name' );
-       }
-
-       /**
-        * Get database id given a user name
-        * @param string $name Username
-        * @param integer $flags User::READ_* constant bitfield
-        * @return int|null The corresponding user's ID, or null if user is nonexistent
-        */
-       public static function idFromName( $name, $flags = self::READ_NORMAL ) {
-               $nt = Title::makeTitleSafe( NS_USER, $name );
-               if ( is_null( $nt ) ) {
-                       // Illegal name
-                       return null;
-               }
-
-               if ( !( $flags & self::READ_LATEST ) && isset( self::$idCacheByName[$name] ) ) {
-                       return self::$idCacheByName[$name];
-               }
-
-               $db = ( $flags & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-
-               $s = $db->selectRow(
-                       'user',
-                       array( 'user_id' ),
-                       array( 'user_name' => $nt->getText() ),
-                       __METHOD__
-               );
-
-               if ( $s === false ) {
-                       $result = null;
-               } else {
-                       $result = $s->user_id;
-               }
-
-               self::$idCacheByName[$name] = $result;
-
-               if ( count( self::$idCacheByName ) > 1000 ) {
-                       self::$idCacheByName = array();
-               }
-
-               return $result;
-       }
-
-       /**
-        * Reset the cache used in idFromName(). For use in tests.
-        */
-       public static function resetIdByNameCache() {
-               self::$idCacheByName = array();
-       }
-
-       /**
-        * Does the string match an anonymous IPv4 address?
-        *
-        * This function exists for username validation, in order to reject
-        * usernames which are similar in form to IP addresses. Strings such
-        * as 300.300.300.300 will return true because it looks like an IP
-        * address, despite not being strictly valid.
-        *
-        * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
-        * address because the usemod software would "cloak" anonymous IP
-        * addresses like this, if we allowed accounts like this to be created
-        * new users could get the old edits of these anonymous users.
-        *
-        * @param string $name Name to match
-        * @return bool
-        */
-       public static function isIP( $name ) {
-               return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
-                       || IP::isIPv6( $name );
-       }
-
-       /**
-        * Is the input a valid username?
-        *
-        * Checks if the input is a valid username, we don't want an empty string,
-        * an IP address, anything that contains slashes (would mess up subpages),
-        * is longer than the maximum allowed username size or doesn't begin with
-        * a capital letter.
-        *
-        * @param string $name Name to match
-        * @return bool
-        */
-       public static function isValidUserName( $name ) {
-               global $wgContLang, $wgMaxNameChars;
-
-               if ( $name == ''
-                       || User::isIP( $name )
-                       || strpos( $name, '/' ) !== false
-                       || strlen( $name ) > $wgMaxNameChars
-                       || $name != $wgContLang->ucfirst( $name )
-               ) {
-                       wfDebugLog( 'username', __METHOD__ .
-                               ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
-                       return false;
-               }
-
-               // Ensure that the name can't be misresolved as a different title,
-               // such as with extra namespace keys at the start.
-               $parsed = Title::newFromText( $name );
-               if ( is_null( $parsed )
-                       || $parsed->getNamespace()
-                       || strcmp( $name, $parsed->getPrefixedText() ) ) {
-                       wfDebugLog( 'username', __METHOD__ .
-                               ": '$name' invalid due to ambiguous prefixes" );
-                       return false;
-               }
-
-               // Check an additional blacklist of troublemaker characters.
-               // Should these be merged into the title char list?
-               $unicodeBlacklist = '/[' .
-                       '\x{0080}-\x{009f}' . # iso-8859-1 control chars
-                       '\x{00a0}' .          # non-breaking space
-                       '\x{2000}-\x{200f}' . # various whitespace
-                       '\x{2028}-\x{202f}' . # breaks and control chars
-                       '\x{3000}' .          # ideographic space
-                       '\x{e000}-\x{f8ff}' . # private use
-                       ']/u';
-               if ( preg_match( $unicodeBlacklist, $name ) ) {
-                       wfDebugLog( 'username', __METHOD__ .
-                               ": '$name' invalid due to blacklisted characters" );
-                       return false;
-               }
-
-               return true;
-       }
-
-       /**
-        * Usernames which fail to pass this function will be blocked
-        * from user login and new account registrations, but may be used
-        * internally by batch processes.
-        *
-        * If an account already exists in this form, login will be blocked
-        * by a failure to pass this function.
-        *
-        * @param string $name Name to match
-        * @return bool
-        */
-       public static function isUsableName( $name ) {
-               global $wgReservedUsernames;
-               // Must be a valid username, obviously ;)
-               if ( !self::isValidUserName( $name ) ) {
-                       return false;
-               }
-
-               static $reservedUsernames = false;
-               if ( !$reservedUsernames ) {
-                       $reservedUsernames = $wgReservedUsernames;
-                       Hooks::run( 'UserGetReservedNames', array( &$reservedUsernames ) );
-               }
-
-               // Certain names may be reserved for batch processes.
-               foreach ( $reservedUsernames as $reserved ) {
-                       if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
-                               $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
-                       }
-                       if ( $reserved == $name ) {
-                               return false;
-                       }
-               }
-               return true;
-       }
-
-       /**
-        * Usernames which fail to pass this function will be blocked
-        * from new account registrations, but may be used internally
-        * either by batch processes or by user accounts which have
-        * already been created.
-        *
-        * Additional blacklisting may be added here rather than in
-        * isValidUserName() to avoid disrupting existing accounts.
-        *
-        * @param string $name String to match
-        * @return bool
-        */
-       public static function isCreatableName( $name ) {
-               global $wgInvalidUsernameCharacters;
-
-               // Ensure that the username isn't longer than 235 bytes, so that
-               // (at least for the builtin skins) user javascript and css files
-               // will work. (bug 23080)
-               if ( strlen( $name ) > 235 ) {
-                       wfDebugLog( 'username', __METHOD__ .
-                               ": '$name' invalid due to length" );
-                       return false;
-               }
-
-               // Preg yells if you try to give it an empty string
-               if ( $wgInvalidUsernameCharacters !== '' ) {
-                       if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
-                               wfDebugLog( 'username', __METHOD__ .
-                                       ": '$name' invalid due to wgInvalidUsernameCharacters" );
-                               return false;
-                       }
-               }
-
-               return self::isUsableName( $name );
-       }
-
-       /**
-        * Is the input a valid password for this user?
-        *
-        * @param string $password Desired password
-        * @return bool
-        */
-       public function isValidPassword( $password ) {
-               // simple boolean wrapper for getPasswordValidity
-               return $this->getPasswordValidity( $password ) === true;
-       }
-
-
-       /**
-        * Given unvalidated password input, return error message on failure.
-        *
-        * @param string $password Desired password
-        * @return bool|string|array True on success, string or array of error message on failure
-        */
-       public function getPasswordValidity( $password ) {
-               $result = $this->checkPasswordValidity( $password );
-               if ( $result->isGood() ) {
-                       return true;
-               } else {
-                       $messages = array();
-                       foreach ( $result->getErrorsByType( 'error' ) as $error ) {
-                               $messages[] = $error['message'];
-                       }
-                       foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
-                               $messages[] = $warning['message'];
-                       }
-                       if ( count( $messages ) === 1 ) {
-                               return $messages[0];
-                       }
-                       return $messages;
-               }
-       }
-
-       /**
-        * Check if this is a valid password for this user
-        *
-        * Create a Status object based on the password's validity.
-        * The Status should be set to fatal if the user should not
-        * be allowed to log in, and should have any errors that
-        * would block changing the password.
-        *
-        * If the return value of this is not OK, the password
-        * should not be checked. If the return value is not Good,
-        * the password can be checked, but the user should not be
-        * able to set their password to this.
-        *
-        * @param string $password Desired password
-        * @param string $purpose one of 'login', 'create', 'reset'
-        * @return Status
-        * @since 1.23
-        */
-       public function checkPasswordValidity( $password, $purpose = 'login' ) {
-               global $wgPasswordPolicy;
-
-               $upp = new UserPasswordPolicy(
-                       $wgPasswordPolicy['policies'],
-                       $wgPasswordPolicy['checks']
-               );
-
-               $status = Status::newGood();
-               $result = false; // init $result to false for the internal checks
-
-               if ( !Hooks::run( 'isValidPassword', array( $password, &$result, $this ) ) ) {
-                       $status->error( $result );
-                       return $status;
-               }
-
-               if ( $result === false ) {
-                       $status->merge( $upp->checkUserPassword( $this, $password, $purpose ) );
-                       return $status;
-               } elseif ( $result === true ) {
-                       return $status;
-               } else {
-                       $status->error( $result );
-                       return $status; // the isValidPassword hook set a string $result and returned true
-               }
-       }
-
-       /**
-        * Given unvalidated user input, return a canonical username, or false if
-        * the username is invalid.
-        * @param string $name User input
-        * @param string|bool $validate Type of validation to use:
-        *   - false        No validation
-        *   - 'valid'      Valid for batch processes
-        *   - 'usable'     Valid for batch processes and login
-        *   - 'creatable'  Valid for batch processes, login and account creation
-        *
-        * @throws InvalidArgumentException
-        * @return bool|string
-        */
-       public static function getCanonicalName( $name, $validate = 'valid' ) {
-               // Force usernames to capital
-               global $wgContLang;
-               $name = $wgContLang->ucfirst( $name );
-
-               # Reject names containing '#'; these will be cleaned up
-               # with title normalisation, but then it's too late to
-               # check elsewhere
-               if ( strpos( $name, '#' ) !== false ) {
-                       return false;
-               }
-
-               // Clean up name according to title rules,
-               // but only when validation is requested (bug 12654)
-               $t = ( $validate !== false ) ?
-                       Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
-               // Check for invalid titles
-               if ( is_null( $t ) ) {
-                       return false;
-               }
-
-               // Reject various classes of invalid names
-               global $wgAuth;
-               $name = $wgAuth->getCanonicalName( $t->getText() );
-
-               switch ( $validate ) {
-                       case false:
-                               break;
-                       case 'valid':
-                               if ( !User::isValidUserName( $name ) ) {
-                                       $name = false;
-                               }
-                               break;
-                       case 'usable':
-                               if ( !User::isUsableName( $name ) ) {
-                                       $name = false;
-                               }
-                               break;
-                       case 'creatable':
-                               if ( !User::isCreatableName( $name ) ) {
-                                       $name = false;
-                               }
-                               break;
-                       default:
-                               throw new InvalidArgumentException(
-                                       'Invalid parameter value for $validate in ' . __METHOD__ );
-               }
-               return $name;
-       }
-
-       /**
-        * Count the number of edits of a user
-        *
-        * @param int $uid User ID to check
-        * @return int The user's edit count
-        *
-        * @deprecated since 1.21 in favour of User::getEditCount
-        */
-       public static function edits( $uid ) {
-               wfDeprecated( __METHOD__, '1.21' );
-               $user = self::newFromId( $uid );
-               return $user->getEditCount();
-       }
-
-       /**
-        * Return a random password.
-        *
-        * @deprecated since 1.27, use PasswordFactory::generateRandomPasswordString()
-        * @return string New random password
-        */
-       public static function randomPassword() {
-               global $wgMinimalPasswordLength;
-               return PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
-       }
-
-       /**
-        * Set cached properties to default.
-        *
-        * @note This no longer clears uncached lazy-initialised properties;
-        *       the constructor does that instead.
-        *
-        * @param string|bool $name
-        */
-       public function loadDefaults( $name = false ) {
-               $this->mId = 0;
-               $this->mName = $name;
-               $this->mRealName = '';
-               $this->mEmail = '';
-               $this->mOptionOverrides = null;
-               $this->mOptionsLoaded = false;
-
-               $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
-               if ( $loggedOut !== null ) {
-                       $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
-               } else {
-                       $this->mTouched = '1'; # Allow any pages to be cached
-               }
-
-               $this->mToken = null; // Don't run cryptographic functions till we need a token
-               $this->mEmailAuthenticated = null;
-               $this->mEmailToken = '';
-               $this->mEmailTokenExpires = null;
-               $this->mRegistration = wfTimestamp( TS_MW );
-               $this->mGroups = array();
-
-               Hooks::run( 'UserLoadDefaults', array( $this, $name ) );
-       }
-
-       /**
-        * Return whether an item has been loaded.
-        *
-        * @param string $item Item to check. Current possibilities:
-        *   - id
-        *   - name
-        *   - realname
-        * @param string $all 'all' to check if the whole object has been loaded
-        *   or any other string to check if only the item is available (e.g.
-        *   for optimisation)
-        * @return bool
-        */
-       public function isItemLoaded( $item, $all = 'all' ) {
-               return ( $this->mLoadedItems === true && $all === 'all' ) ||
-                       ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
-       }
-
-       /**
-        * Set that an item has been loaded
-        *
-        * @param string $item
-        */
-       protected function setItemLoaded( $item ) {
-               if ( is_array( $this->mLoadedItems ) ) {
-                       $this->mLoadedItems[$item] = true;
-               }
-       }
-
-       /**
-        * Load user data from the session or login cookie.
-        *
-        * @return bool True if the user is logged in, false otherwise.
-        */
-       private function loadFromSession() {
-               $result = null;
-               Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
-               if ( $result !== null ) {
-                       return $result;
-               }
-
-               $request = $this->getRequest();
-
-               $cookieId = $request->getCookie( 'UserID' );
-               $sessId = $request->getSessionData( 'wsUserID' );
-
-               if ( $cookieId !== null ) {
-                       $sId = intval( $cookieId );
-                       if ( $sessId !== null && $cookieId != $sessId ) {
-                               wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
-                                       cookie user ID ($sId) don't match!" );
-                               return false;
-                       }
-                       $request->setSessionData( 'wsUserID', $sId );
-               } elseif ( $sessId !== null && $sessId != 0 ) {
-                       $sId = $sessId;
-               } else {
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsUserName' ) !== null ) {
-                       $sName = $request->getSessionData( 'wsUserName' );
-               } elseif ( $request->getCookie( 'UserName' ) !== null ) {
-                       $sName = $request->getCookie( 'UserName' );
-                       $request->setSessionData( 'wsUserName', $sName );
-               } else {
-                       return false;
-               }
-
-               $proposedUser = User::newFromId( $sId );
-               if ( !$proposedUser->isLoggedIn() ) {
-                       // Not a valid ID
-                       return false;
-               }
-
-               global $wgBlockDisablesLogin;
-               if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
-                       // User blocked and we've disabled blocked user logins
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsToken' ) ) {
-                       $passwordCorrect =
-                               ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
-                       $from = 'session';
-               } elseif ( $request->getCookie( 'Token' ) ) {
-                       # Get the token from DB/cache and clean it up to remove garbage padding.
-                       # This deals with historical problems with bugs and the default column value.
-                       $token = rtrim( $proposedUser->getToken( false ) ); // correct token
-                       // Make comparison in constant time (bug 61346)
-                       $passwordCorrect = strlen( $token )
-                               && hash_equals( $token, $request->getCookie( 'Token' ) );
-                       $from = 'cookie';
-               } else {
-                       // No session or persistent login cookie
-                       return false;
-               }
-
-               if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
-                       $this->loadFromUserObject( $proposedUser );
-                       $request->setSessionData( 'wsToken', $this->mToken );
-                       wfDebug( "User: logged in from $from\n" );
-                       return true;
-               } else {
-                       // Invalid credentials
-                       wfDebug( "User: can't log in from $from, invalid credentials\n" );
-                       return false;
-               }
-       }
-
-       /**
-        * Load user and user_group data from the database.
-        * $this->mId must be set, this is how the user is identified.
-        *
-        * @param integer $flags User::READ_* constant bitfield
-        * @return bool True if the user exists, false if the user is anonymous
-        */
-       public function loadFromDatabase( $flags = self::READ_LATEST ) {
-               // Paranoia
-               $this->mId = intval( $this->mId );
-
-               // Anonymous user
-               if ( !$this->mId ) {
-                       $this->loadDefaults();
-                       return false;
-               }
-
-               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
-               $db = wfGetDB( $index );
-
-               $s = $db->selectRow(
-                       'user',
-                       self::selectFields(),
-                       array( 'user_id' => $this->mId ),
-                       __METHOD__,
-                       $options
-               );
-
-               $this->queryFlagsUsed = $flags;
-               Hooks::run( 'UserLoadFromDatabase', array( $this, &$s ) );
-
-               if ( $s !== false ) {
-                       // Initialise user table data
-                       $this->loadFromRow( $s );
-                       $this->mGroups = null; // deferred
-                       $this->getEditCount(); // revalidation for nulls
-                       return true;
-               } else {
-                       // Invalid user_id
-                       $this->mId = 0;
-                       $this->loadDefaults();
-                       return false;
-               }
-       }
-
-       /**
-        * Initialize this object from a row from the user table.
-        *
-        * @param stdClass $row Row from the user table to load.
-        * @param array $data Further user data to load into the object
-        *
-        *      user_groups             Array with groups out of the user_groups table
-        *      user_properties         Array with properties out of the user_properties table
-        */
-       protected function loadFromRow( $row, $data = null ) {
-               $all = true;
-
-               $this->mGroups = null; // deferred
-
-               if ( isset( $row->user_name ) ) {
-                       $this->mName = $row->user_name;
-                       $this->mFrom = 'name';
-                       $this->setItemLoaded( 'name' );
-               } else {
-                       $all = false;
-               }
-
-               if ( isset( $row->user_real_name ) ) {
-                       $this->mRealName = $row->user_real_name;
-                       $this->setItemLoaded( 'realname' );
-               } else {
-                       $all = false;
-               }
-
-               if ( isset( $row->user_id ) ) {
-                       $this->mId = intval( $row->user_id );
-                       $this->mFrom = 'id';
-                       $this->setItemLoaded( 'id' );
-               } else {
-                       $all = false;
-               }
-
-               if ( isset( $row->user_id ) && isset( $row->user_name ) ) {
-                       self::$idCacheByName[$row->user_name] = $row->user_id;
-               }
-
-               if ( isset( $row->user_editcount ) ) {
-                       $this->mEditCount = $row->user_editcount;
-               } else {
-                       $all = false;
-               }
-
-               if ( isset( $row->user_email ) ) {
-                       $this->mEmail = $row->user_email;
-                       $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
-                       $this->mToken = $row->user_token;
-                       if ( $this->mToken == '' ) {
-                               $this->mToken = null;
-                       }
-                       $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
-                       $this->mEmailToken = $row->user_email_token;
-                       $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
-                       $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
-               } else {
-                       $all = false;
-               }
-
-               if ( $all ) {
-                       $this->mLoadedItems = true;
-               }
-
-               if ( is_array( $data ) ) {
-                       if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
-                               $this->mGroups = $data['user_groups'];
-                       }
-                       if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
-                               $this->loadOptions( $data['user_properties'] );
-                       }
-               }
-       }
-
-       /**
-        * Load the data for this user object from another user object.
-        *
-        * @param User $user
-        */
-       protected function loadFromUserObject( $user ) {
-               $user->load();
-               $user->loadGroups();
-               $user->loadOptions();
-               foreach ( self::$mCacheVars as $var ) {
-                       $this->$var = $user->$var;
-               }
-       }
-
-       /**
-        * Load the groups from the database if they aren't already loaded.
-        */
-       private function loadGroups() {
-               if ( is_null( $this->mGroups ) ) {
-                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                               ? wfGetDB( DB_MASTER )
-                               : wfGetDB( DB_SLAVE );
-                       $res = $db->select( 'user_groups',
-                               array( 'ug_group' ),
-                               array( 'ug_user' => $this->mId ),
-                               __METHOD__ );
-                       $this->mGroups = array();
-                       foreach ( $res as $row ) {
-                               $this->mGroups[] = $row->ug_group;
-                       }
-               }
-       }
-
-       /**
-        * Add the user to the group if he/she meets given criteria.
-        *
-        * Contrary to autopromotion by \ref $wgAutopromote, the group will be
-        *   possible to remove manually via Special:UserRights. In such case it
-        *   will not be re-added automatically. The user will also not lose the
-        *   group if they no longer meet the criteria.
-        *
-        * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria)
-        *
-        * @return array Array of groups the user has been promoted to.
-        *
-        * @see $wgAutopromoteOnce
-        */
-       public function addAutopromoteOnceGroups( $event ) {
-               global $wgAutopromoteOnceLogInRC, $wgAuth;
-
-               if ( wfReadOnly() || !$this->getId() ) {
-                       return array();
-               }
-
-               $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
-               if ( !count( $toPromote ) ) {
-                       return array();
-               }
-
-               if ( !$this->checkAndSetTouched() ) {
-                       return array(); // raced out (bug T48834)
-               }
-
-               $oldGroups = $this->getGroups(); // previous groups
-               foreach ( $toPromote as $group ) {
-                       $this->addGroup( $group );
-               }
-               // update groups in external authentication database
-               Hooks::run( 'UserGroupsChanged', array( $this, $toPromote, array(), false ) );
-               $wgAuth->updateExternalDBGroups( $this, $toPromote );
-
-               $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
-
-               $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
-               $logEntry->setPerformer( $this );
-               $logEntry->setTarget( $this->getUserPage() );
-               $logEntry->setParameters( array(
-                       '4::oldgroups' => $oldGroups,
-                       '5::newgroups' => $newGroups,
-               ) );
-               $logid = $logEntry->insert();
-               if ( $wgAutopromoteOnceLogInRC ) {
-                       $logEntry->publish( $logid );
-               }
-
-               return $toPromote;
-       }
-
-       /**
-        * Bump user_touched if it didn't change since this object was loaded
-        *
-        * On success, the mTouched field is updated.
-        * The user serialization cache is always cleared.
-        *
-        * @return bool Whether user_touched was actually updated
-        * @since 1.26
-        */
-       protected function checkAndSetTouched() {
-               $this->load();
-
-               if ( !$this->mId ) {
-                       return false; // anon
-               }
-
-               // Get a new user_touched that is higher than the old one
-               $oldTouched = $this->mTouched;
-               $newTouched = $this->newTouchedTimestamp();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update( 'user',
-                       array( 'user_touched' => $dbw->timestamp( $newTouched ) ),
-                       array(
-                               'user_id' => $this->mId,
-                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
-                       ),
-                       __METHOD__
-               );
-               $success = ( $dbw->affectedRows() > 0 );
-
-               if ( $success ) {
-                       $this->mTouched = $newTouched;
-                       $this->clearSharedCache();
-               } else {
-                       // Clears on failure too since that is desired if the cache is stale
-                       $this->clearSharedCache( 'refresh' );
-               }
-
-               return $success;
-       }
-
-       /**
-        * Clear various cached data stored in this object. The cache of the user table
-        * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
-        *
-        * @param bool|string $reloadFrom Reload user and user_groups table data from a
-        *   given source. May be "name", "id", "defaults", "session", or false for no reload.
-        */
-       public function clearInstanceCache( $reloadFrom = false ) {
-               $this->mNewtalk = -1;
-               $this->mDatePreference = null;
-               $this->mBlockedby = -1; # Unset
-               $this->mHash = false;
-               $this->mRights = null;
-               $this->mEffectiveGroups = null;
-               $this->mImplicitGroups = null;
-               $this->mGroups = null;
-               $this->mOptions = null;
-               $this->mOptionsLoaded = false;
-               $this->mEditCount = null;
-
-               if ( $reloadFrom ) {
-                       $this->mLoadedItems = array();
-                       $this->mFrom = $reloadFrom;
-               }
-       }
-
-       /**
-        * Combine the language default options with any site-specific options
-        * and add the default language variants.
-        *
-        * @return array Array of String options
-        */
-       public static function getDefaultOptions() {
-               global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
-
-               static $defOpt = null;
-               if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
-                       // Disabling this for the unit tests, as they rely on being able to change $wgContLang
-                       // mid-request and see that change reflected in the return value of this function.
-                       // Which is insane and would never happen during normal MW operation
-                       return $defOpt;
-               }
-
-               $defOpt = $wgDefaultUserOptions;
-               // Default language setting
-               $defOpt['language'] = $wgContLang->getCode();
-               foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
-                       $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
-               }
-               foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
-                       $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
-               }
-               $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
-
-               Hooks::run( 'UserGetDefaultOptions', array( &$defOpt ) );
-
-               return $defOpt;
-       }
-
-       /**
-        * Get a given default option value.
-        *
-        * @param string $opt Name of option to retrieve
-        * @return string Default option value
-        */
-       public static function getDefaultOption( $opt ) {
-               $defOpts = self::getDefaultOptions();
-               if ( isset( $defOpts[$opt] ) ) {
-                       return $defOpts[$opt];
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Get blocking information
-        * @param bool $bFromSlave Whether to check the slave database first.
-        *   To improve performance, non-critical checks are done against slaves.
-        *   Check when actually saving should be done against master.
-        */
-       private function getBlockedStatus( $bFromSlave = true ) {
-               global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
-
-               if ( -1 != $this->mBlockedby ) {
-                       return;
-               }
-
-               wfDebug( __METHOD__ . ": checking...\n" );
-
-               // Initialize data...
-               // Otherwise something ends up stomping on $this->mBlockedby when
-               // things get lazy-loaded later, causing false positive block hits
-               // due to -1 !== 0. Probably session-related... Nothing should be
-               // overwriting mBlockedby, surely?
-               $this->load();
-
-               # We only need to worry about passing the IP address to the Block generator if the
-               # user is not immune to autoblocks/hardblocks, and they are the current user so we
-               # know which IP address they're actually coming from
-               if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->equals( $wgUser ) ) {
-                       $ip = $this->getRequest()->getIP();
-               } else {
-                       $ip = null;
-               }
-
-               // User/IP blocking
-               $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
-
-               // Proxy blocking
-               if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
-                       && !in_array( $ip, $wgProxyWhitelist )
-               ) {
-                       // Local list
-                       if ( self::isLocallyBlockedProxy( $ip ) ) {
-                               $block = new Block;
-                               $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
-                               $block->mReason = wfMessage( 'proxyblockreason' )->text();
-                               $block->setTarget( $ip );
-                       } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
-                               $block = new Block;
-                               $block->setBlocker( wfMessage( 'sorbs' )->text() );
-                               $block->mReason = wfMessage( 'sorbsreason' )->text();
-                               $block->setTarget( $ip );
-                       }
-               }
-
-               // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
-               if ( !$block instanceof Block
-                       && $wgApplyIpBlocksToXff
-                       && $ip !== null
-                       && !$this->isAllowed( 'proxyunbannable' )
-                       && !in_array( $ip, $wgProxyWhitelist )
-               ) {
-                       $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
-                       $xff = array_map( 'trim', explode( ',', $xff ) );
-                       $xff = array_diff( $xff, array( $ip ) );
-                       $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
-                       $block = Block::chooseBlock( $xffblocks, $xff );
-                       if ( $block instanceof Block ) {
-                               # Mangle the reason to alert the user that the block
-                               # originated from matching the X-Forwarded-For header.
-                               $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
-                       }
-               }
-
-               if ( $block instanceof Block ) {
-                       wfDebug( __METHOD__ . ": Found block.\n" );
-                       $this->mBlock = $block;
-                       $this->mBlockedby = $block->getByName();
-                       $this->mBlockreason = $block->mReason;
-                       $this->mHideName = $block->mHideName;
-                       $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
-               } else {
-                       $this->mBlockedby = '';
-                       $this->mHideName = 0;
-                       $this->mAllowUsertalk = false;
-               }
-
-               // Extensions
-               Hooks::run( 'GetBlockedStatus', array( &$this ) );
-
-       }
-
-       /**
-        * Whether the given IP is in a DNS blacklist.
-        *
-        * @param string $ip IP to check
-        * @param bool $checkWhitelist Whether to check the whitelist first
-        * @return bool True if blacklisted.
-        */
-       public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
-               global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
-
-               if ( !$wgEnableDnsBlacklist ) {
-                       return false;
-               }
-
-               if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
-                       return false;
-               }
-
-               return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
-       }
-
-       /**
-        * Whether the given IP is in a given DNS blacklist.
-        *
-        * @param string $ip IP to check
-        * @param string|array $bases Array of Strings: URL of the DNS blacklist
-        * @return bool True if blacklisted.
-        */
-       public function inDnsBlacklist( $ip, $bases ) {
-
-               $found = false;
-               // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
-               if ( IP::isIPv4( $ip ) ) {
-                       // Reverse IP, bug 21255
-                       $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
-
-                       foreach ( (array)$bases as $base ) {
-                               // Make hostname
-                               // If we have an access key, use that too (ProjectHoneypot, etc.)
-                               $basename = $base;
-                               if ( is_array( $base ) ) {
-                                       if ( count( $base ) >= 2 ) {
-                                               // Access key is 1, base URL is 0
-                                               $host = "{$base[1]}.$ipReversed.{$base[0]}";
-                                       } else {
-                                               $host = "$ipReversed.{$base[0]}";
-                                       }
-                                       $basename = $base[0];
-                               } else {
-                                       $host = "$ipReversed.$base";
-                               }
-
-                               // Send query
-                               $ipList = gethostbynamel( $host );
-
-                               if ( $ipList ) {
-                                       wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
-                                       $found = true;
-                                       break;
-                               } else {
-                                       wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
-                               }
-                       }
-               }
-
-               return $found;
-       }
-
-       /**
-        * Check if an IP address is in the local proxy list
-        *
-        * @param string $ip
-        *
-        * @return bool
-        */
-       public static function isLocallyBlockedProxy( $ip ) {
-               global $wgProxyList;
-
-               if ( !$wgProxyList ) {
-                       return false;
-               }
-
-               if ( !is_array( $wgProxyList ) ) {
-                       // Load from the specified file
-                       $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
-               }
-
-               if ( !is_array( $wgProxyList ) ) {
-                       $ret = false;
-               } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
-                       $ret = true;
-               } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
-                       // Old-style flipped proxy list
-                       $ret = true;
-               } else {
-                       $ret = false;
-               }
-               return $ret;
-       }
-
-       /**
-        * Is this user subject to rate limiting?
-        *
-        * @return bool True if rate limited
-        */
-       public function isPingLimitable() {
-               global $wgRateLimitsExcludedIPs;
-               if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
-                       // No other good way currently to disable rate limits
-                       // for specific IPs. :P
-                       // But this is a crappy hack and should die.
-                       return false;
-               }
-               return !$this->isAllowed( 'noratelimit' );
-       }
-
-       /**
-        * Primitive rate limits: enforce maximum actions per time period
-        * to put a brake on flooding.
-        *
-        * The method generates both a generic profiling point and a per action one
-        * (suffix being "-$action".
-        *
-        * @note When using a shared cache like memcached, IP-address
-        * last-hit counters will be shared across wikis.
-        *
-        * @param string $action Action to enforce; 'edit' if unspecified
-        * @param int $incrBy Positive amount to increment counter by [defaults to 1]
-        * @return bool True if a rate limiter was tripped
-        */
-       public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
-               // Call the 'PingLimiter' hook
-               $result = false;
-               if ( !Hooks::run( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
-                       return $result;
-               }
-
-               global $wgRateLimits;
-               if ( !isset( $wgRateLimits[$action] ) ) {
-                       return false;
-               }
-
-               // Some groups shouldn't trigger the ping limiter, ever
-               if ( !$this->isPingLimitable() ) {
-                       return false;
-               }
-
-               $limits = $wgRateLimits[$action];
-               $keys = array();
-               $id = $this->getId();
-               $userLimit = false;
-
-               if ( isset( $limits['anon'] ) && $id == 0 ) {
-                       $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
-               }
-
-               if ( isset( $limits['user'] ) && $id != 0 ) {
-                       $userLimit = $limits['user'];
-               }
-               if ( $this->isNewbie() ) {
-                       if ( isset( $limits['newbie'] ) && $id != 0 ) {
-                               $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
-                       }
-                       if ( isset( $limits['ip'] ) ) {
-                               $ip = $this->getRequest()->getIP();
-                               $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
-                       }
-                       if ( isset( $limits['subnet'] ) ) {
-                               $ip = $this->getRequest()->getIP();
-                               $matches = array();
-                               $subnet = false;
-                               if ( IP::isIPv6( $ip ) ) {
-                                       $parts = IP::parseRange( "$ip/64" );
-                                       $subnet = $parts[0];
-                               } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
-                                       // IPv4
-                                       $subnet = $matches[1];
-                               }
-                               if ( $subnet !== false ) {
-                                       $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
-                               }
-                       }
-               }
-               // Check for group-specific permissions
-               // If more than one group applies, use the group with the highest limit
-               foreach ( $this->getGroups() as $group ) {
-                       if ( isset( $limits[$group] ) ) {
-                               if ( $userLimit === false
-                                       || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
-                               ) {
-                                       $userLimit = $limits[$group];
-                               }
-                       }
-               }
-               // Set the user limit key
-               if ( $userLimit !== false ) {
-                       list( $max, $period ) = $userLimit;
-                       wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
-                       $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
-               }
-
-               $cache = ObjectCache::getLocalClusterInstance();
-
-               $triggered = false;
-               foreach ( $keys as $key => $limit ) {
-                       list( $max, $period ) = $limit;
-                       $summary = "(limit $max in {$period}s)";
-                       $count = $cache->get( $key );
-                       // Already pinged?
-                       if ( $count ) {
-                               if ( $count >= $max ) {
-                                       wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
-                                               "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
-                                       $triggered = true;
-                               } else {
-                                       wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
-                               }
-                       } else {
-                               wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
-                               if ( $incrBy > 0 ) {
-                                       $cache->add( $key, 0, intval( $period ) ); // first ping
-                               }
-                       }
-                       if ( $incrBy > 0 ) {
-                               $cache->incr( $key, $incrBy );
-                       }
-               }
-
-               return $triggered;
-       }
-
-       /**
-        * Check if user is blocked
-        *
-        * @param bool $bFromSlave Whether to check the slave database instead of
-        *   the master. Hacked from false due to horrible probs on site.
-        * @return bool True if blocked, false otherwise
-        */
-       public function isBlocked( $bFromSlave = true ) {
-               return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
-       }
-
-       /**
-        * Get the block affecting the user, or null if the user is not blocked
-        *
-        * @param bool $bFromSlave Whether to check the slave database instead of the master
-        * @return Block|null
-        */
-       public function getBlock( $bFromSlave = true ) {
-               $this->getBlockedStatus( $bFromSlave );
-               return $this->mBlock instanceof Block ? $this->mBlock : null;
-       }
-
-       /**
-        * Check if user is blocked from editing a particular article
-        *
-        * @param Title $title Title to check
-        * @param bool $bFromSlave Whether to check the slave database instead of the master
-        * @return bool
-        */
-       public function isBlockedFrom( $title, $bFromSlave = false ) {
-               global $wgBlockAllowsUTEdit;
-
-               $blocked = $this->isBlocked( $bFromSlave );
-               $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
-               // If a user's name is suppressed, they cannot make edits anywhere
-               if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
-                       && $title->getNamespace() == NS_USER_TALK ) {
-                       $blocked = false;
-                       wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
-               }
-
-               Hooks::run( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
-
-               return $blocked;
-       }
-
-       /**
-        * If user is blocked, return the name of the user who placed the block
-        * @return string Name of blocker
-        */
-       public function blockedBy() {
-               $this->getBlockedStatus();
-               return $this->mBlockedby;
-       }
-
-       /**
-        * If user is blocked, return the specified reason for the block
-        * @return string Blocking reason
-        */
-       public function blockedFor() {
-               $this->getBlockedStatus();
-               return $this->mBlockreason;
-       }
-
-       /**
-        * If user is blocked, return the ID for the block
-        * @return int Block ID
-        */
-       public function getBlockId() {
-               $this->getBlockedStatus();
-               return ( $this->mBlock ? $this->mBlock->getId() : false );
-       }
-
-       /**
-        * Check if user is blocked on all wikis.
-        * Do not use for actual edit permission checks!
-        * This is intended for quick UI checks.
-        *
-        * @param string $ip IP address, uses current client if none given
-        * @return bool True if blocked, false otherwise
-        */
-       public function isBlockedGlobally( $ip = '' ) {
-               if ( $this->mBlockedGlobally !== null ) {
-                       return $this->mBlockedGlobally;
-               }
-               // User is already an IP?
-               if ( IP::isIPAddress( $this->getName() ) ) {
-                       $ip = $this->getName();
-               } elseif ( !$ip ) {
-                       $ip = $this->getRequest()->getIP();
-               }
-               $blocked = false;
-               Hooks::run( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
-               $this->mBlockedGlobally = (bool)$blocked;
-               return $this->mBlockedGlobally;
-       }
-
-       /**
-        * Check if user account is locked
-        *
-        * @return bool True if locked, false otherwise
-        */
-       public function isLocked() {
-               if ( $this->mLocked !== null ) {
-                       return $this->mLocked;
-               }
-               global $wgAuth;
-               $authUser = $wgAuth->getUserInstance( $this );
-               $this->mLocked = (bool)$authUser->isLocked();
-               Hooks::run( 'UserIsLocked', array( $this, &$this->mLocked ) );
-               return $this->mLocked;
-       }
-
-       /**
-        * Check if user account is hidden
-        *
-        * @return bool True if hidden, false otherwise
-        */
-       public function isHidden() {
-               if ( $this->mHideName !== null ) {
-                       return $this->mHideName;
-               }
-               $this->getBlockedStatus();
-               if ( !$this->mHideName ) {
-                       global $wgAuth;
-                       $authUser = $wgAuth->getUserInstance( $this );
-                       $this->mHideName = (bool)$authUser->isHidden();
-                       Hooks::run( 'UserIsHidden', array( $this, &$this->mHideName ) );
-               }
-               return $this->mHideName;
-       }
-
-       /**
-        * Get the user's ID.
-        * @return int The user's ID; 0 if the user is anonymous or nonexistent
-        */
-       public function getId() {
-               if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
-                       // Special case, we know the user is anonymous
-                       return 0;
-               } elseif ( !$this->isItemLoaded( 'id' ) ) {
-                       // Don't load if this was initialized from an ID
-                       $this->load();
-               }
-               return $this->mId;
-       }
-
-       /**
-        * Set the user and reload all fields according to a given ID
-        * @param int $v User ID to reload
-        */
-       public function setId( $v ) {
-               $this->mId = $v;
-               $this->clearInstanceCache( 'id' );
-       }
-
-       /**
-        * Get the user name, or the IP of an anonymous user
-        * @return string User's name or IP address
-        */
-       public function getName() {
-               if ( $this->isItemLoaded( 'name', 'only' ) ) {
-                       // Special case optimisation
-                       return $this->mName;
-               } else {
-                       $this->load();
-                       if ( $this->mName === false ) {
-                               // Clean up IPs
-                               $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
-                       }
-                       return $this->mName;
-               }
-       }
-
-       /**
-        * Set the user name.
-        *
-        * This does not reload fields from the database according to the given
-        * name. Rather, it is used to create a temporary "nonexistent user" for
-        * later addition to the database. It can also be used to set the IP
-        * address for an anonymous user to something other than the current
-        * remote IP.
-        *
-        * @note User::newFromName() has roughly the same function, when the named user
-        * does not exist.
-        * @param string $str New user name to set
-        */
-       public function setName( $str ) {
-               $this->load();
-               $this->mName = $str;
-       }
-
-       /**
-        * Get the user's name escaped by underscores.
-        * @return string Username escaped by underscores.
-        */
-       public function getTitleKey() {
-               return str_replace( ' ', '_', $this->getName() );
-       }
-
-       /**
-        * Check if the user has new messages.
-        * @return bool True if the user has new messages
-        */
-       public function getNewtalk() {
-               $this->load();
-
-               // Load the newtalk status if it is unloaded (mNewtalk=-1)
-               if ( $this->mNewtalk === -1 ) {
-                       $this->mNewtalk = false; # reset talk page status
-
-                       // Check memcached separately for anons, who have no
-                       // entire User object stored in there.
-                       if ( !$this->mId ) {
-                               global $wgDisableAnonTalk;
-                               if ( $wgDisableAnonTalk ) {
-                                       // Anon newtalk disabled by configuration.
-                                       $this->mNewtalk = false;
-                               } else {
-                                       $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
-                               }
-                       } else {
-                               $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
-                       }
-               }
-
-               return (bool)$this->mNewtalk;
-       }
-
-       /**
-        * Return the data needed to construct links for new talk page message
-        * alerts. If there are new messages, this will return an associative array
-        * with the following data:
-        *     wiki: The database name of the wiki
-        *     link: Root-relative link to the user's talk page
-        *     rev: The last talk page revision that the user has seen or null. This
-        *         is useful for building diff links.
-        * If there are no new messages, it returns an empty array.
-        * @note This function was designed to accomodate multiple talk pages, but
-        * currently only returns a single link and revision.
-        * @return array
-        */
-       public function getNewMessageLinks() {
-               $talks = array();
-               if ( !Hooks::run( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
-                       return $talks;
-               } elseif ( !$this->getNewtalk() ) {
-                       return array();
-               }
-               $utp = $this->getTalkPage();
-               $dbr = wfGetDB( DB_SLAVE );
-               // Get the "last viewed rev" timestamp from the oldest message notification
-               $timestamp = $dbr->selectField( 'user_newtalk',
-                       'MIN(user_last_timestamp)',
-                       $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
-                       __METHOD__ );
-               $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
-               return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
-       }
-
-       /**
-        * Get the revision ID for the last talk page revision viewed by the talk
-        * page owner.
-        * @return int|null Revision ID or null
-        */
-       public function getNewMessageRevisionId() {
-               $newMessageRevisionId = null;
-               $newMessageLinks = $this->getNewMessageLinks();
-               if ( $newMessageLinks ) {
-                       // Note: getNewMessageLinks() never returns more than a single link
-                       // and it is always for the same wiki, but we double-check here in
-                       // case that changes some time in the future.
-                       if ( count( $newMessageLinks ) === 1
-                               && $newMessageLinks[0]['wiki'] === wfWikiID()
-                               && $newMessageLinks[0]['rev']
-                       ) {
-                               /** @var Revision $newMessageRevision */
-                               $newMessageRevision = $newMessageLinks[0]['rev'];
-                               $newMessageRevisionId = $newMessageRevision->getId();
-                       }
-               }
-               return $newMessageRevisionId;
-       }
-
-       /**
-        * Internal uncached check for new messages
-        *
-        * @see getNewtalk()
-        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
-        * @param string|int $id User's IP address for anonymous users, User ID otherwise
-        * @return bool True if the user has new messages
-        */
-       protected function checkNewtalk( $field, $id ) {
-               $dbr = wfGetDB( DB_SLAVE );
-
-               $ok = $dbr->selectField( 'user_newtalk', $field, array( $field => $id ), __METHOD__ );
-
-               return $ok !== false;
-       }
-
-       /**
-        * Add or update the new messages flag
-        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
-        * @param string|int $id User's IP address for anonymous users, User ID otherwise
-        * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null.
-        * @return bool True if successful, false otherwise
-        */
-       protected function updateNewtalk( $field, $id, $curRev = null ) {
-               // Get timestamp of the talk page revision prior to the current one
-               $prevRev = $curRev ? $curRev->getPrevious() : false;
-               $ts = $prevRev ? $prevRev->getTimestamp() : null;
-               // Mark the user as having new messages since this revision
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->insert( 'user_newtalk',
-                       array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
-                       __METHOD__,
-                       'IGNORE' );
-               if ( $dbw->affectedRows() ) {
-                       wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
-                       return true;
-               } else {
-                       wfDebug( __METHOD__ . " already set ($field, $id)\n" );
-                       return false;
-               }
-       }
-
-       /**
-        * Clear the new messages flag for the given user
-        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
-        * @param string|int $id User's IP address for anonymous users, User ID otherwise
-        * @return bool True if successful, false otherwise
-        */
-       protected function deleteNewtalk( $field, $id ) {
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->delete( 'user_newtalk',
-                       array( $field => $id ),
-                       __METHOD__ );
-               if ( $dbw->affectedRows() ) {
-                       wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
-                       return true;
-               } else {
-                       wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
-                       return false;
-               }
-       }
-
-       /**
-        * Update the 'You have new messages!' status.
-        * @param bool $val Whether the user has new messages
-        * @param Revision $curRev New, as yet unseen revision of the user talk
-        *   page. Ignored if null or !$val.
-        */
-       public function setNewtalk( $val, $curRev = null ) {
-               if ( wfReadOnly() ) {
-                       return;
-               }
-
-               $this->load();
-               $this->mNewtalk = $val;
-
-               if ( $this->isAnon() ) {
-                       $field = 'user_ip';
-                       $id = $this->getName();
-               } else {
-                       $field = 'user_id';
-                       $id = $this->getId();
-               }
-
-               if ( $val ) {
-                       $changed = $this->updateNewtalk( $field, $id, $curRev );
-               } else {
-                       $changed = $this->deleteNewtalk( $field, $id );
-               }
-
-               if ( $changed ) {
-                       $this->invalidateCache();
-               }
-       }
-
-       /**
-        * Generate a current or new-future timestamp to be stored in the
-        * user_touched field when we update things.
-        * @return string Timestamp in TS_MW format
-        */
-       private function newTouchedTimestamp() {
-               global $wgClockSkewFudge;
-
-               $time = wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
-               if ( $this->mTouched && $time <= $this->mTouched ) {
-                       $time = wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
-               }
-
-               return $time;
-       }
-
-       /**
-        * Clear user data from memcached
-        *
-        * Use after applying updates to the database; caller's
-        * responsibility to update user_touched if appropriate.
-        *
-        * Called implicitly from invalidateCache() and saveSettings().
-        *
-        * @param string $mode Use 'refresh' to clear now; otherwise before DB commit
-        */
-       public function clearSharedCache( $mode = 'changed' ) {
-               if ( !$this->getId() ) {
-                       return;
-               }
-
-               $cache = ObjectCache::getMainWANInstance();
-               $key = $this->getCacheKey( $cache );
-               if ( $mode === 'refresh' ) {
-                       $cache->delete( $key, 1 );
-               } else {
-                       wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) {
-                               $cache->delete( $key );
-                       } );
-               }
-       }
-
-       /**
-        * Immediately touch the user data cache for this account
-        *
-        * Calls touch() and removes account data from memcached
-        */
-       public function invalidateCache() {
-               $this->touch();
-               $this->clearSharedCache();
-       }
-
-       /**
-        * Update the "touched" timestamp for the user
-        *
-        * This is useful on various login/logout events when making sure that
-        * a browser or proxy that has multiple tenants does not suffer cache
-        * pollution where the new user sees the old users content. The value
-        * of getTouched() is checked when determining 304 vs 200 responses.
-        * Unlike invalidateCache(), this preserves the User object cache and
-        * avoids database writes.
-        *
-        * @since 1.25
-        */
-       public function touch() {
-               $id = $this->getId();
-               if ( $id ) {
-                       $key = wfMemcKey( 'user-quicktouched', 'id', $id );
-                       ObjectCache::getMainWANInstance()->touchCheckKey( $key );
-                       $this->mQuickTouched = null;
-               }
-       }
-
-       /**
-        * Validate the cache for this account.
-        * @param string $timestamp A timestamp in TS_MW format
-        * @return bool
-        */
-       public function validateCache( $timestamp ) {
-               return ( $timestamp >= $this->getTouched() );
-       }
-
-       /**
-        * Get the user touched timestamp
-        *
-        * Use this value only to validate caches via inequalities
-        * such as in the case of HTTP If-Modified-Since response logic
-        *
-        * @return string TS_MW Timestamp
-        */
-       public function getTouched() {
-               $this->load();
-
-               if ( $this->mId ) {
-                       if ( $this->mQuickTouched === null ) {
-                               $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
-                               $cache = ObjectCache::getMainWANInstance();
-
-                               $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
-                       }
-
-                       return max( $this->mTouched, $this->mQuickTouched );
-               }
-
-               return $this->mTouched;
-       }
-
-       /**
-        * Get the user_touched timestamp field (time of last DB updates)
-        * @return string TS_MW Timestamp
-        * @since 1.26
-        */
-       public function getDBTouched() {
-               $this->load();
-
-               return $this->mTouched;
-       }
-
-       /**
-        * @deprecated Removed in 1.27.
-        * @return Password
-        * @since 1.24
-        */
-       public function getPassword() {
-               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
-       }
-
-       /**
-        * @deprecated Removed in 1.27.
-        * @return Password
-        * @since 1.24
-        */
-       public function getTemporaryPassword() {
-               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
-       }
-
-       /**
-        * Set the password and reset the random token.
-        * Calls through to authentication plugin if necessary;
-        * will have no effect if the auth plugin refuses to
-        * pass the change through or if the legal password
-        * checks fail.
-        *
-        * As a special case, setting the password to null
-        * wipes it, so the account cannot be logged in until
-        * a new password is set, for instance via e-mail.
-        *
-        * @deprecated since 1.27. AuthManager is coming.
-        * @param string $str New password to set
-        * @throws PasswordError On failure
-        * @return bool
-        */
-       public function setPassword( $str ) {
-               global $wgAuth;
-
-               if ( $str !== null ) {
-                       if ( !$wgAuth->allowPasswordChange() ) {
-                               throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
-                       }
-
-                       $status = $this->checkPasswordValidity( $str );
-                       if ( !$status->isGood() ) {
-                               throw new PasswordError( $status->getMessage()->text() );
-                       }
-               }
-
-               if ( !$wgAuth->setPassword( $this, $str ) ) {
-                       throw new PasswordError( wfMessage( 'externaldberror' )->text() );
-               }
-
-               $this->setToken();
-               $this->setOption( 'watchlisttoken', false );
-               $this->setPasswordInternal( $str );
-
-               return true;
-       }
-
-       /**
-        * Set the password and reset the random token unconditionally.
-        *
-        * @deprecated since 1.27. AuthManager is coming.
-        * @param string|null $str New password to set or null to set an invalid
-        *  password hash meaning that the user will not be able to log in
-        *  through the web interface.
-        */
-       public function setInternalPassword( $str ) {
-               global $wgAuth;
-
-               if ( $wgAuth->allowSetLocalPassword() ) {
-                       $this->setToken();
-                       $this->setOption( 'watchlisttoken', false );
-                       $this->setPasswordInternal( $str );
-               }
-       }
-
-       /**
-        * Actually set the password and such
-        * @since 1.27 cannot set a password for a user not in the database
-        * @param string|null $str New password to set or null to set an invalid
-        *  password hash meaning that the user will not be able to log in
-        *  through the web interface.
-        */
-       private function setPasswordInternal( $str ) {
-               $id = self::idFromName( $this->getName(), self::READ_LATEST );
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
-               }
-
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       array(
-                               'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
-                               'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
-                               'user_newpass_time' => $dbw->timestampOrNull( null ),
-                       ),
-                       array(
-                               'user_id' => $id,
-                       ),
-                       __METHOD__
-               );
-       }
-
-       /**
-        * Get the user's current token.
-        * @param bool $forceCreation Force the generation of a new token if the
-        *   user doesn't have one (default=true for backwards compatibility).
-        * @return string Token
-        */
-       public function getToken( $forceCreation = true ) {
-               $this->load();
-               if ( !$this->mToken && $forceCreation ) {
-                       $this->setToken();
-               }
-               return $this->mToken;
-       }
-
-       /**
-        * Set the random token (used for persistent authentication)
-        * Called from loadDefaults() among other places.
-        *
-        * @param string|bool $token If specified, set the token to this value
-        */
-       public function setToken( $token = false ) {
-               $this->load();
-               if ( !$token ) {
-                       $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
-               } else {
-                       $this->mToken = $token;
-               }
-       }
-
-       /**
-        * Set the password for a password reminder or new account email
-        *
-        * @deprecated since 1.27, AuthManager is coming
-        * @param string $str New password to set or null to set an invalid
-        *  password hash meaning that the user will not be able to use it
-        * @param bool $throttle If true, reset the throttle timestamp to the present
-        */
-       public function setNewpassword( $str, $throttle = true ) {
-               $id = $this->getId();
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $update = array(
-                       'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
-               );
-
-               if ( $str === null ) {
-                       $update['user_newpass_time'] = null;
-               } elseif ( $throttle ) {
-                       $update['user_newpass_time'] = $dbw->timestamp();
-               }
-
-               $dbw->update( 'user', $update, array( 'user_id' => $id ), __METHOD__ );
-       }
-
-       /**
-        * Has password reminder email been sent within the last
-        * $wgPasswordReminderResendTime hours?
-        * @return bool
-        */
-       public function isPasswordReminderThrottled() {
-               global $wgPasswordReminderResendTime;
-
-               if ( !$wgPasswordReminderResendTime ) {
-                       return false;
-               }
-
-               $this->load();
-
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-               $newpassTime = $db->selectField(
-                       'user',
-                       'user_newpass_time',
-                       array( 'user_id' => $this->getId() ),
-                       __METHOD__
-               );
-
-               if ( $newpassTime === null ) {
-                       return false;
-               }
-               $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
-               return time() < $expiry;
-       }
-
-       /**
-        * Get the user's e-mail address
-        * @return string User's email address
-        */
-       public function getEmail() {
-               $this->load();
-               Hooks::run( 'UserGetEmail', array( $this, &$this->mEmail ) );
-               return $this->mEmail;
-       }
-
-       /**
-        * Get the timestamp of the user's e-mail authentication
-        * @return string TS_MW timestamp
-        */
-       public function getEmailAuthenticationTimestamp() {
-               $this->load();
-               Hooks::run( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
-               return $this->mEmailAuthenticated;
-       }
-
-       /**
-        * Set the user's e-mail address
-        * @param string $str New e-mail address
-        */
-       public function setEmail( $str ) {
-               $this->load();
-               if ( $str == $this->mEmail ) {
-                       return;
-               }
-               $this->invalidateEmail();
-               $this->mEmail = $str;
-               Hooks::run( 'UserSetEmail', array( $this, &$this->mEmail ) );
-       }
-
-       /**
-        * Set the user's e-mail address and a confirmation mail if needed.
-        *
-        * @since 1.20
-        * @param string $str New e-mail address
-        * @return Status
-        */
-       public function setEmailWithConfirmation( $str ) {
-               global $wgEnableEmail, $wgEmailAuthentication;
-
-               if ( !$wgEnableEmail ) {
-                       return Status::newFatal( 'emaildisabled' );
-               }
-
-               $oldaddr = $this->getEmail();
-               if ( $str === $oldaddr ) {
-                       return Status::newGood( true );
-               }
-
-               $this->setEmail( $str );
-
-               if ( $str !== '' && $wgEmailAuthentication ) {
-                       // Send a confirmation request to the new address if needed
-                       $type = $oldaddr != '' ? 'changed' : 'set';
-                       $result = $this->sendConfirmationMail( $type );
-                       if ( $result->isGood() ) {
-                               // Say to the caller that a confirmation mail has been sent
-                               $result->value = 'eauth';
-                       }
-               } else {
-                       $result = Status::newGood( true );
-               }
-
-               return $result;
-       }
-
-       /**
-        * Get the user's real name
-        * @return string User's real name
-        */
-       public function getRealName() {
-               if ( !$this->isItemLoaded( 'realname' ) ) {
-                       $this->load();
-               }
-
-               return $this->mRealName;
-       }
-
-       /**
-        * Set the user's real name
-        * @param string $str New real name
-        */
-       public function setRealName( $str ) {
-               $this->load();
-               $this->mRealName = $str;
-       }
-
-       /**
-        * Get the user's current setting for a given option.
-        *
-        * @param string $oname The option to check
-        * @param string $defaultOverride A default value returned if the option does not exist
-        * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
-        * @return string User's current value for the option
-        * @see getBoolOption()
-        * @see getIntOption()
-        */
-       public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
-               global $wgHiddenPrefs;
-               $this->loadOptions();
-
-               # We want 'disabled' preferences to always behave as the default value for
-               # users, even if they have set the option explicitly in their settings (ie they
-               # set it, and then it was disabled removing their ability to change it).  But
-               # we don't want to erase the preferences in the database in case the preference
-               # is re-enabled again.  So don't touch $mOptions, just override the returned value
-               if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
-                       return self::getDefaultOption( $oname );
-               }
-
-               if ( array_key_exists( $oname, $this->mOptions ) ) {
-                       return $this->mOptions[$oname];
-               } else {
-                       return $defaultOverride;
-               }
-       }
-
-       /**
-        * Get all user's options
-        *
-        * @param int $flags Bitwise combination of:
-        *   User::GETOPTIONS_EXCLUDE_DEFAULTS  Exclude user options that are set
-        *                                      to the default value. (Since 1.25)
-        * @return array
-        */
-       public function getOptions( $flags = 0 ) {
-               global $wgHiddenPrefs;
-               $this->loadOptions();
-               $options = $this->mOptions;
-
-               # We want 'disabled' preferences to always behave as the default value for
-               # users, even if they have set the option explicitly in their settings (ie they
-               # set it, and then it was disabled removing their ability to change it).  But
-               # we don't want to erase the preferences in the database in case the preference
-               # is re-enabled again.  So don't touch $mOptions, just override the returned value
-               foreach ( $wgHiddenPrefs as $pref ) {
-                       $default = self::getDefaultOption( $pref );
-                       if ( $default !== null ) {
-                               $options[$pref] = $default;
-                       }
-               }
-
-               if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
-                       $options = array_diff_assoc( $options, self::getDefaultOptions() );
-               }
-
-               return $options;
-       }
-
-       /**
-        * Get the user's current setting for a given option, as a boolean value.
-        *
-        * @param string $oname The option to check
-        * @return bool User's current value for the option
-        * @see getOption()
-        */
-       public function getBoolOption( $oname ) {
-               return (bool)$this->getOption( $oname );
-       }
-
-       /**
-        * Get the user's current setting for a given option, as an integer value.
-        *
-        * @param string $oname The option to check
-        * @param int $defaultOverride A default value returned if the option does not exist
-        * @return int User's current value for the option
-        * @see getOption()
-        */
-       public function getIntOption( $oname, $defaultOverride = 0 ) {
-               $val = $this->getOption( $oname );
-               if ( $val == '' ) {
-                       $val = $defaultOverride;
-               }
-               return intval( $val );
-       }
-
-       /**
-        * Set the given option for a user.
-        *
-        * You need to call saveSettings() to actually write to the database.
-        *
-        * @param string $oname The option to set
-        * @param mixed $val New value to set
-        */
-       public function setOption( $oname, $val ) {
-               $this->loadOptions();
-
-               // Explicitly NULL values should refer to defaults
-               if ( is_null( $val ) ) {
-                       $val = self::getDefaultOption( $oname );
-               }
-
-               $this->mOptions[$oname] = $val;
-       }
-
-       /**
-        * Get a token stored in the preferences (like the watchlist one),
-        * resetting it if it's empty (and saving changes).
-        *
-        * @param string $oname The option name to retrieve the token from
-        * @return string|bool User's current value for the option, or false if this option is disabled.
-        * @see resetTokenFromOption()
-        * @see getOption()
-        * @deprecated 1.26 Applications should use the OAuth extension
-        */
-       public function getTokenFromOption( $oname ) {
-               global $wgHiddenPrefs;
-
-               $id = $this->getId();
-               if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
-                       return false;
-               }
-
-               $token = $this->getOption( $oname );
-               if ( !$token ) {
-                       // Default to a value based on the user token to avoid space
-                       // wasted on storing tokens for all users. When this option
-                       // is set manually by the user, only then is it stored.
-                       $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
-               }
-
-               return $token;
-       }
-
-       /**
-        * Reset a token stored in the preferences (like the watchlist one).
-        * *Does not* save user's preferences (similarly to setOption()).
-        *
-        * @param string $oname The option name to reset the token in
-        * @return string|bool New token value, or false if this option is disabled.
-        * @see getTokenFromOption()
-        * @see setOption()
-        */
-       public function resetTokenFromOption( $oname ) {
-               global $wgHiddenPrefs;
-               if ( in_array( $oname, $wgHiddenPrefs ) ) {
-                       return false;
-               }
-
-               $token = MWCryptRand::generateHex( 40 );
-               $this->setOption( $oname, $token );
-               return $token;
-       }
-
-       /**
-        * Return a list of the types of user options currently returned by
-        * User::getOptionKinds().
-        *
-        * Currently, the option kinds are:
-        * - 'registered' - preferences which are registered in core MediaWiki or
-        *                  by extensions using the UserGetDefaultOptions hook.
-        * - 'registered-multiselect' - as above, using the 'multiselect' type.
-        * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
-        * - 'userjs' - preferences with names starting with 'userjs-', intended to
-        *              be used by user scripts.
-        * - 'special' - "preferences" that are not accessible via User::getOptions
-        *               or User::setOptions.
-        * - 'unused' - preferences about which MediaWiki doesn't know anything.
-        *              These are usually legacy options, removed in newer versions.
-        *
-        * The API (and possibly others) use this function to determine the possible
-        * option types for validation purposes, so make sure to update this when a
-        * new option kind is added.
-        *
-        * @see User::getOptionKinds
-        * @return array Option kinds
-        */
-       public static function listOptionKinds() {
-               return array(
-                       'registered',
-                       'registered-multiselect',
-                       'registered-checkmatrix',
-                       'userjs',
-                       'special',
-                       'unused'
-               );
-       }
-
-       /**
-        * Return an associative array mapping preferences keys to the kind of a preference they're
-        * used for. Different kinds are handled differently when setting or reading preferences.
-        *
-        * See User::listOptionKinds for the list of valid option types that can be provided.
-        *
-        * @see User::listOptionKinds
-        * @param IContextSource $context
-        * @param array $options Assoc. array with options keys to check as keys.
-        *   Defaults to $this->mOptions.
-        * @return array The key => kind mapping data
-        */
-       public function getOptionKinds( IContextSource $context, $options = null ) {
-               $this->loadOptions();
-               if ( $options === null ) {
-                       $options = $this->mOptions;
-               }
-
-               $prefs = Preferences::getPreferences( $this, $context );
-               $mapping = array();
-
-               // Pull out the "special" options, so they don't get converted as
-               // multiselect or checkmatrix.
-               $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
-               foreach ( $specialOptions as $name => $value ) {
-                       unset( $prefs[$name] );
-               }
-
-               // Multiselect and checkmatrix options are stored in the database with
-               // one key per option, each having a boolean value. Extract those keys.
-               $multiselectOptions = array();
-               foreach ( $prefs as $name => $info ) {
-                       if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
-                                       ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
-                               $opts = HTMLFormField::flattenOptions( $info['options'] );
-                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
-
-                               foreach ( $opts as $value ) {
-                                       $multiselectOptions["$prefix$value"] = true;
-                               }
-
-                               unset( $prefs[$name] );
-                       }
-               }
-               $checkmatrixOptions = array();
-               foreach ( $prefs as $name => $info ) {
-                       if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
-                                       ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
-                               $columns = HTMLFormField::flattenOptions( $info['columns'] );
-                               $rows = HTMLFormField::flattenOptions( $info['rows'] );
-                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
-
-                               foreach ( $columns as $column ) {
-                                       foreach ( $rows as $row ) {
-                                               $checkmatrixOptions["$prefix$column-$row"] = true;
-                                       }
-                               }
-
-                               unset( $prefs[$name] );
-                       }
-               }
-
-               // $value is ignored
-               foreach ( $options as $key => $value ) {
-                       if ( isset( $prefs[$key] ) ) {
-                               $mapping[$key] = 'registered';
-                       } elseif ( isset( $multiselectOptions[$key] ) ) {
-                               $mapping[$key] = 'registered-multiselect';
-                       } elseif ( isset( $checkmatrixOptions[$key] ) ) {
-                               $mapping[$key] = 'registered-checkmatrix';
-                       } elseif ( isset( $specialOptions[$key] ) ) {
-                               $mapping[$key] = 'special';
-                       } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
-                               $mapping[$key] = 'userjs';
-                       } else {
-                               $mapping[$key] = 'unused';
-                       }
-               }
-
-               return $mapping;
-       }
-
-       /**
-        * Reset certain (or all) options to the site defaults
-        *
-        * The optional parameter determines which kinds of preferences will be reset.
-        * Supported values are everything that can be reported by getOptionKinds()
-        * and 'all', which forces a reset of *all* preferences and overrides everything else.
-        *
-        * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
-        *  array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
-        *  for backwards-compatibility.
-        * @param IContextSource|null $context Context source used when $resetKinds
-        *  does not contain 'all', passed to getOptionKinds().
-        *  Defaults to RequestContext::getMain() when null.
-        */
-       public function resetOptions(
-               $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
-               IContextSource $context = null
-       ) {
-               $this->load();
-               $defaultOptions = self::getDefaultOptions();
-
-               if ( !is_array( $resetKinds ) ) {
-                       $resetKinds = array( $resetKinds );
-               }
-
-               if ( in_array( 'all', $resetKinds ) ) {
-                       $newOptions = $defaultOptions;
-               } else {
-                       if ( $context === null ) {
-                               $context = RequestContext::getMain();
-                       }
-
-                       $optionKinds = $this->getOptionKinds( $context );
-                       $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
-                       $newOptions = array();
-
-                       // Use default values for the options that should be deleted, and
-                       // copy old values for the ones that shouldn't.
-                       foreach ( $this->mOptions as $key => $value ) {
-                               if ( in_array( $optionKinds[$key], $resetKinds ) ) {
-                                       if ( array_key_exists( $key, $defaultOptions ) ) {
-                                               $newOptions[$key] = $defaultOptions[$key];
-                                       }
-                               } else {
-                                       $newOptions[$key] = $value;
-                               }
-                       }
-               }
-
-               Hooks::run( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
-
-               $this->mOptions = $newOptions;
-               $this->mOptionsLoaded = true;
-       }
-
-       /**
-        * Get the user's preferred date format.
-        * @return string User's preferred date format
-        */
-       public function getDatePreference() {
-               // Important migration for old data rows
-               if ( is_null( $this->mDatePreference ) ) {
-                       global $wgLang;
-                       $value = $this->getOption( 'date' );
-                       $map = $wgLang->getDatePreferenceMigrationMap();
-                       if ( isset( $map[$value] ) ) {
-                               $value = $map[$value];
-                       }
-                       $this->mDatePreference = $value;
-               }
-               return $this->mDatePreference;
-       }
-
-       /**
-        * Determine based on the wiki configuration and the user's options,
-        * whether this user must be over HTTPS no matter what.
-        *
-        * @return bool
-        */
-       public function requiresHTTPS() {
-               global $wgSecureLogin;
-               if ( !$wgSecureLogin ) {
-                       return false;
-               } else {
-                       $https = $this->getBoolOption( 'prefershttps' );
-                       Hooks::run( 'UserRequiresHTTPS', array( $this, &$https ) );
-                       if ( $https ) {
-                               $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
-                       }
-                       return $https;
-               }
-       }
-
-       /**
-        * Get the user preferred stub threshold
-        *
-        * @return int
-        */
-       public function getStubThreshold() {
-               global $wgMaxArticleSize; # Maximum article size, in Kb
-               $threshold = $this->getIntOption( 'stubthreshold' );
-               if ( $threshold > $wgMaxArticleSize * 1024 ) {
-                       // If they have set an impossible value, disable the preference
-                       // so we can use the parser cache again.
-                       $threshold = 0;
-               }
-               return $threshold;
-       }
-
-       /**
-        * Get the permissions this user has.
-        * @return array Array of String permission names
-        */
-       public function getRights() {
-               if ( is_null( $this->mRights ) ) {
-                       $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
-                       Hooks::run( 'UserGetRights', array( $this, &$this->mRights ) );
-                       // Force reindexation of rights when a hook has unset one of them
-                       $this->mRights = array_values( array_unique( $this->mRights ) );
-               }
-               return $this->mRights;
-       }
-
-       /**
-        * Get the list of explicit group memberships this user has.
-        * The implicit * and user groups are not included.
-        * @return array Array of String internal group names
-        */
-       public function getGroups() {
-               $this->load();
-               $this->loadGroups();
-               return $this->mGroups;
-       }
-
-       /**
-        * Get the list of implicit group memberships this user has.
-        * This includes all explicit groups, plus 'user' if logged in,
-        * '*' for all accounts, and autopromoted groups
-        * @param bool $recache Whether to avoid the cache
-        * @return array Array of String internal group names
-        */
-       public function getEffectiveGroups( $recache = false ) {
-               if ( $recache || is_null( $this->mEffectiveGroups ) ) {
-                       $this->mEffectiveGroups = array_unique( array_merge(
-                               $this->getGroups(), // explicit groups
-                               $this->getAutomaticGroups( $recache ) // implicit groups
-                       ) );
-                       // Hook for additional groups
-                       Hooks::run( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
-                       // Force reindexation of groups when a hook has unset one of them
-                       $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
-               }
-               return $this->mEffectiveGroups;
-       }
-
-       /**
-        * Get the list of implicit group memberships this user has.
-        * This includes 'user' if logged in, '*' for all accounts,
-        * and autopromoted groups
-        * @param bool $recache Whether to avoid the cache
-        * @return array Array of String internal group names
-        */
-       public function getAutomaticGroups( $recache = false ) {
-               if ( $recache || is_null( $this->mImplicitGroups ) ) {
-                       $this->mImplicitGroups = array( '*' );
-                       if ( $this->getId() ) {
-                               $this->mImplicitGroups[] = 'user';
-
-                               $this->mImplicitGroups = array_unique( array_merge(
-                                       $this->mImplicitGroups,
-                                       Autopromote::getAutopromoteGroups( $this )
-                               ) );
-                       }
-                       if ( $recache ) {
-                               // Assure data consistency with rights/groups,
-                               // as getEffectiveGroups() depends on this function
-                               $this->mEffectiveGroups = null;
-                       }
-               }
-               return $this->mImplicitGroups;
-       }
-
-       /**
-        * Returns the groups the user has belonged to.
-        *
-        * The user may still belong to the returned groups. Compare with getGroups().
-        *
-        * The function will not return groups the user had belonged to before MW 1.17
-        *
-        * @return array Names of the groups the user has belonged to.
-        */
-       public function getFormerGroups() {
-               $this->load();
-
-               if ( is_null( $this->mFormerGroups ) ) {
-                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                               ? wfGetDB( DB_MASTER )
-                               : wfGetDB( DB_SLAVE );
-                       $res = $db->select( 'user_former_groups',
-                               array( 'ufg_group' ),
-                               array( 'ufg_user' => $this->mId ),
-                               __METHOD__ );
-                       $this->mFormerGroups = array();
-                       foreach ( $res as $row ) {
-                               $this->mFormerGroups[] = $row->ufg_group;
-                       }
-               }
-
-               return $this->mFormerGroups;
-       }
-
-       /**
-        * Get the user's edit count.
-        * @return int|null Null for anonymous users
-        */
-       public function getEditCount() {
-               if ( !$this->getId() ) {
-                       return null;
-               }
-
-               if ( $this->mEditCount === null ) {
-                       /* Populate the count, if it has not been populated yet */
-                       $dbr = wfGetDB( DB_SLAVE );
-                       // check if the user_editcount field has been initialized
-                       $count = $dbr->selectField(
-                               'user', 'user_editcount',
-                               array( 'user_id' => $this->mId ),
-                               __METHOD__
-                       );
-
-                       if ( $count === null ) {
-                               // it has not been initialized. do so.
-                               $count = $this->initEditCount();
-                       }
-                       $this->mEditCount = $count;
-               }
-               return (int)$this->mEditCount;
-       }
-
-       /**
-        * Add the user to the given group.
-        * This takes immediate effect.
-        * @param string $group Name of the group to add
-        * @return bool
-        */
-       public function addGroup( $group ) {
-               $this->load();
-
-               if ( !Hooks::run( 'UserAddGroup', array( $this, &$group ) ) ) {
-                       return false;
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               if ( $this->getId() ) {
-                       $dbw->insert( 'user_groups',
-                               array(
-                                       'ug_user' => $this->getID(),
-                                       'ug_group' => $group,
-                               ),
-                               __METHOD__,
-                               array( 'IGNORE' ) );
-               }
-
-               $this->loadGroups();
-               $this->mGroups[] = $group;
-               // In case loadGroups was not called before, we now have the right twice.
-               // Get rid of the duplicate.
-               $this->mGroups = array_unique( $this->mGroups );
-
-               // Refresh the groups caches, and clear the rights cache so it will be
-               // refreshed on the next call to $this->getRights().
-               $this->getEffectiveGroups( true );
-               $this->mRights = null;
-
-               $this->invalidateCache();
-
-               return true;
-       }
-
-       /**
-        * Remove the user from the given group.
-        * This takes immediate effect.
-        * @param string $group Name of the group to remove
-        * @return bool
-        */
-       public function removeGroup( $group ) {
-               $this->load();
-               if ( !Hooks::run( 'UserRemoveGroup', array( $this, &$group ) ) ) {
-                       return false;
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->delete( 'user_groups',
-                       array(
-                               'ug_user' => $this->getID(),
-                               'ug_group' => $group,
-                       ), __METHOD__
-               );
-               // Remember that the user was in this group
-               $dbw->insert( 'user_former_groups',
-                       array(
-                               'ufg_user' => $this->getID(),
-                               'ufg_group' => $group,
-                       ),
-                       __METHOD__,
-                       array( 'IGNORE' )
-               );
-
-               $this->loadGroups();
-               $this->mGroups = array_diff( $this->mGroups, array( $group ) );
-
-               // Refresh the groups caches, and clear the rights cache so it will be
-               // refreshed on the next call to $this->getRights().
-               $this->getEffectiveGroups( true );
-               $this->mRights = null;
-
-               $this->invalidateCache();
-
-               return true;
-       }
-
-       /**
-        * Get whether the user is logged in
-        * @return bool
-        */
-       public function isLoggedIn() {
-               return $this->getID() != 0;
-       }
-
-       /**
-        * Get whether the user is anonymous
-        * @return bool
-        */
-       public function isAnon() {
-               return !$this->isLoggedIn();
-       }
-
-       /**
-        * Check if user is allowed to access a feature / make an action
-        *
-        * @param string ... Permissions to test
-        * @return bool True if user is allowed to perform *any* of the given actions
-        */
-       public function isAllowedAny() {
-               $permissions = func_get_args();
-               foreach ( $permissions as $permission ) {
-                       if ( $this->isAllowed( $permission ) ) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        *
-        * @param string ... Permissions to test
-        * @return bool True if the user is allowed to perform *all* of the given actions
-        */
-       public function isAllowedAll() {
-               $permissions = func_get_args();
-               foreach ( $permissions as $permission ) {
-                       if ( !$this->isAllowed( $permission ) ) {
-                               return false;
-                       }
-               }
-               return true;
-       }
-
-       /**
-        * Internal mechanics of testing a permission
-        * @param string $action
-        * @return bool
-        */
-       public function isAllowed( $action = '' ) {
-               if ( $action === '' ) {
-                       return true; // In the spirit of DWIM
-               }
-               // Patrolling may not be enabled
-               if ( $action === 'patrol' || $action === 'autopatrol' ) {
-                       global $wgUseRCPatrol, $wgUseNPPatrol;
-                       if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
-                               return false;
-                       }
-               }
-               // Use strict parameter to avoid matching numeric 0 accidentally inserted
-               // by misconfiguration: 0 == 'foo'
-               return in_array( $action, $this->getRights(), true );
-       }
-
-       /**
-        * Check whether to enable recent changes patrol features for this user
-        * @return bool True or false
-        */
-       public function useRCPatrol() {
-               global $wgUseRCPatrol;
-               return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
-       }
-
-       /**
-        * Check whether to enable new pages patrol features for this user
-        * @return bool True or false
-        */
-       public function useNPPatrol() {
-               global $wgUseRCPatrol, $wgUseNPPatrol;
-               return (
-                       ( $wgUseRCPatrol || $wgUseNPPatrol )
-                               && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
-               );
-       }
-
-       /**
-        * Get the WebRequest object to use with this object
-        *
-        * @return WebRequest
-        */
-       public function getRequest() {
-               if ( $this->mRequest ) {
-                       return $this->mRequest;
-               } else {
-                       global $wgRequest;
-                       return $wgRequest;
-               }
-       }
-
-       /**
-        * Get the current skin, loading it if required
-        * @return Skin The current skin
-        * @todo FIXME: Need to check the old failback system [AV]
-        * @deprecated since 1.18 Use ->getSkin() in the most relevant outputting context you have
-        */
-       public function getSkin() {
-               wfDeprecated( __METHOD__, '1.18' );
-               return RequestContext::getMain()->getSkin();
-       }
-
-       /**
-        * Get a WatchedItem for this user and $title.
-        *
-        * @since 1.22 $checkRights parameter added
-        * @param Title $title
-        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
-        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
-        * @return WatchedItem
-        */
-       public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
-               $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
-
-               if ( isset( $this->mWatchedItems[$key] ) ) {
-                       return $this->mWatchedItems[$key];
-               }
-
-               if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
-                       $this->mWatchedItems = array();
-               }
-
-               $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
-               return $this->mWatchedItems[$key];
-       }
-
-       /**
-        * Check the watched status of an article.
-        * @since 1.22 $checkRights parameter added
-        * @param Title $title Title of the article to look at
-        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
-        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
-        * @return bool
-        */
-       public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
-               return $this->getWatchedItem( $title, $checkRights )->isWatched();
-       }
-
-       /**
-        * Watch an article.
-        * @since 1.22 $checkRights parameter added
-        * @param Title $title Title of the article to look at
-        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
-        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
-        */
-       public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
-               $this->getWatchedItem( $title, $checkRights )->addWatch();
-               $this->invalidateCache();
-       }
-
-       /**
-        * Stop watching an article.
-        * @since 1.22 $checkRights parameter added
-        * @param Title $title Title of the article to look at
-        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
-        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
-        */
-       public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
-               $this->getWatchedItem( $title, $checkRights )->removeWatch();
-               $this->invalidateCache();
-       }
-
-       /**
-        * Clear the user's notification timestamp for the given title.
-        * If e-notif e-mails are on, they will receive notification mails on
-        * the next change of the page if it's watched etc.
-        * @note If the user doesn't have 'editmywatchlist', this will do nothing.
-        * @param Title $title Title of the article to look at
-        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
-        */
-       public function clearNotification( &$title, $oldid = 0 ) {
-               global $wgUseEnotif, $wgShowUpdatedMarker;
-
-               // Do nothing if the database is locked to writes
-               if ( wfReadOnly() ) {
-                       return;
-               }
-
-               // Do nothing if not allowed to edit the watchlist
-               if ( !$this->isAllowed( 'editmywatchlist' ) ) {
-                       return;
-               }
-
-               // If we're working on user's talk page, we should update the talk page message indicator
-               if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
-                       if ( !Hooks::run( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
-                               return;
-                       }
-
-                       $that = $this;
-                       // Try to update the DB post-send and only if needed...
-                       DeferredUpdates::addCallableUpdate( function() use ( $that, $title, $oldid ) {
-                               if ( !$that->getNewtalk() ) {
-                                       return; // no notifications to clear
-                               }
-
-                               // Delete the last notifications (they stack up)
-                               $that->setNewtalk( false );
-
-                               // If there is a new, unseen, revision, use its timestamp
-                               $nextid = $oldid
-                                       ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
-                                       : null;
-                               if ( $nextid ) {
-                                       $that->setNewtalk( true, Revision::newFromId( $nextid ) );
-                               }
-                       } );
-               }
-
-               if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
-                       return;
-               }
-
-               if ( $this->isAnon() ) {
-                       // Nothing else to do...
-                       return;
-               }
-
-               // Only update the timestamp if the page is being watched.
-               // The query to find out if it is watched is cached both in memcached and per-invocation,
-               // and when it does have to be executed, it can be on a slave
-               // If this is the user's newtalk page, we always update the timestamp
-               $force = '';
-               if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
-                       $force = 'force';
-               }
-
-               $this->getWatchedItem( $title )->resetNotificationTimestamp(
-                       $force, $oldid, WatchedItem::DEFERRED
-               );
-       }
-
-       /**
-        * Resets all of the given user's page-change notification timestamps.
-        * If e-notif e-mails are on, they will receive notification mails on
-        * the next change of any watched page.
-        * @note If the user doesn't have 'editmywatchlist', this will do nothing.
-        */
-       public function clearAllNotifications() {
-               if ( wfReadOnly() ) {
-                       return;
-               }
-
-               // Do nothing if not allowed to edit the watchlist
-               if ( !$this->isAllowed( 'editmywatchlist' ) ) {
-                       return;
-               }
-
-               global $wgUseEnotif, $wgShowUpdatedMarker;
-               if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
-                       $this->setNewtalk( false );
-                       return;
-               }
-               $id = $this->getId();
-               if ( $id != 0 ) {
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->update( 'watchlist',
-                               array( /* SET */ 'wl_notificationtimestamp' => null ),
-                               array( /* WHERE */ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ),
-                               __METHOD__
-                       );
-                       // We also need to clear here the "you have new message" notification for the own user_talk page;
-                       // it's cleared one page view later in WikiPage::doViewUpdates().
-               }
-       }
-
-       /**
-        * Set a cookie on the user's client. Wrapper for
-        * WebResponse::setCookie
-        * @param string $name Name of the cookie to set
-        * @param string $value Value to set
-        * @param int $exp Expiration time, as a UNIX time value;
-        *                   if 0 or not specified, use the default $wgCookieExpiration
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        * @param array $params Array of options sent passed to WebResponse::setcookie()
-        * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
-        *        is passed.
-        */
-       protected function setCookie(
-               $name, $value, $exp = 0, $secure = null, $params = array(), $request = null
-       ) {
-               if ( $request === null ) {
-                       $request = $this->getRequest();
-               }
-               $params['secure'] = $secure;
-               $request->response()->setCookie( $name, $value, $exp, $params );
-       }
-
-       /**
-        * Clear a cookie on the user's client
-        * @param string $name Name of the cookie to clear
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        * @param array $params Array of options sent passed to WebResponse::setcookie()
-        */
-       protected function clearCookie( $name, $secure = null, $params = array() ) {
-               $this->setCookie( $name, '', time() - 86400, $secure, $params );
-       }
-
-       /**
-        * Set an extended login cookie on the user's client. The expiry of the cookie
-        * is controlled by the $wgExtendedLoginCookieExpiration configuration
-        * variable.
-        *
-        * @see User::setCookie
-        *
-        * @param string $name Name of the cookie to set
-        * @param string $value Value to set
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        */
-       protected function setExtendedLoginCookie( $name, $value, $secure ) {
-               global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
-
-               $exp = time();
-               $exp += $wgExtendedLoginCookieExpiration !== null
-                       ? $wgExtendedLoginCookieExpiration
-                       : $wgCookieExpiration;
-
-               $this->setCookie( $name, $value, $exp, $secure );
-       }
-
-       /**
-        * Set the default cookies for this session on the user's client.
-        *
-        * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
-        *        is passed.
-        * @param bool $secure Whether to force secure/insecure cookies or use default
-        * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
-        */
-       public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
-               global $wgExtendedLoginCookies;
-
-               if ( $request === null ) {
-                       $request = $this->getRequest();
-               }
-
-               $this->load();
-               if ( 0 == $this->mId ) {
-                       return;
-               }
-               if ( !$this->mToken ) {
-                       // When token is empty or NULL generate a new one and then save it to the database
-                       // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
-                       // Simply by setting every cell in the user_token column to NULL and letting them be
-                       // regenerated as users log back into the wiki.
-                       $this->setToken();
-                       if ( !wfReadOnly() ) {
-                               $this->saveSettings();
-                       }
-               }
-               $session = array(
-                       'wsUserID' => $this->mId,
-                       'wsToken' => $this->mToken,
-                       'wsUserName' => $this->getName()
-               );
-               $cookies = array(
-                       'UserID' => $this->mId,
-                       'UserName' => $this->getName(),
-               );
-               if ( $rememberMe ) {
-                       $cookies['Token'] = $this->mToken;
-               } else {
-                       $cookies['Token'] = false;
-               }
-
-               Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
-
-               foreach ( $session as $name => $value ) {
-                       $request->setSessionData( $name, $value );
-               }
-               foreach ( $cookies as $name => $value ) {
-                       if ( $value === false ) {
-                               $this->clearCookie( $name );
-                       } elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
-                               $this->setExtendedLoginCookie( $name, $value, $secure );
-                       } else {
-                               $this->setCookie( $name, $value, 0, $secure, array(), $request );
-                       }
-               }
-
-               /**
-                * If wpStickHTTPS was selected, also set an insecure cookie that
-                * will cause the site to redirect the user to HTTPS, if they access
-                * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
-                * as the one set by centralauth (bug 53538). Also set it to session, or
-                * standard time setting, based on if rememberme was set.
-                */
-               if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
-                       $this->setCookie(
-                               'forceHTTPS',
-                               'true',
-                               $rememberMe ? 0 : null,
-                               false,
-                               array( 'prefix' => '' ) // no prefix
-                       );
-               }
-       }
-
-       /**
-        * Log this user out.
-        */
-       public function logout() {
-               if ( Hooks::run( 'UserLogout', array( &$this ) ) ) {
-                       $this->doLogout();
-               }
-       }
-
-       /**
-        * Clear the user's cookies and session, and reset the instance cache.
-        * @see logout()
-        */
-       public function doLogout() {
-               $this->clearInstanceCache( 'defaults' );
-
-               $this->getRequest()->setSessionData( 'wsUserID', 0 );
-
-               $this->clearCookie( 'UserID' );
-               $this->clearCookie( 'Token' );
-               $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
-
-               // Remember when user logged out, to prevent seeing cached pages
-               $this->setCookie( 'LoggedOut', time(), time() + 86400 );
-       }
-
-       /**
-        * Save this user's settings into the database.
-        * @todo Only rarely do all these fields need to be set!
-        */
-       public function saveSettings() {
-               if ( wfReadOnly() ) {
-                       // @TODO: caller should deal with this instead!
-                       // This should really just be an exception.
-                       MWExceptionHandler::logException( new DBExpectedError(
-                               null,
-                               "Could not update user with ID '{$this->mId}'; DB is read-only."
-                       ) );
-                       return;
-               }
-
-               $this->load();
-               if ( 0 == $this->mId ) {
-                       return; // anon
-               }
-
-               // Get a new user_touched that is higher than the old one.
-               // This will be used for a CAS check as a last-resort safety
-               // check against race conditions and slave lag.
-               $oldTouched = $this->mTouched;
-               $newTouched = $this->newTouchedTimestamp();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update( 'user',
-                       array( /* SET */
-                               'user_name' => $this->mName,
-                               'user_real_name' => $this->mRealName,
-                               'user_email' => $this->mEmail,
-                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
-                               'user_touched' => $dbw->timestamp( $newTouched ),
-                               'user_token' => strval( $this->mToken ),
-                               'user_email_token' => $this->mEmailToken,
-                               'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
-                       ), array( /* WHERE */
-                               'user_id' => $this->mId,
-                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
-                       ), __METHOD__
-               );
-
-               if ( !$dbw->affectedRows() ) {
-                       // Maybe the problem was a missed cache update; clear it to be safe
-                       $this->clearSharedCache( 'refresh' );
-                       // User was changed in the meantime or loaded with stale data
-                       $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'slave';
-                       throw new MWException(
-                               "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
-                               " the version of the user to be saved is older than the current version."
-                       );
-               }
-
-               $this->mTouched = $newTouched;
-               $this->saveOptions();
-
-               Hooks::run( 'UserSaveSettings', array( $this ) );
-               $this->clearSharedCache();
-               $this->getUserPage()->invalidateCache();
-       }
-
-       /**
-        * If only this user's username is known, and it exists, return the user ID.
-        *
-        * @param int $flags Bitfield of User:READ_* constants; useful for existence checks
-        * @return int
-        */
-       public function idForName( $flags = 0 ) {
-               $s = trim( $this->getName() );
-               if ( $s === '' ) {
-                       return 0;
-               }
-
-               $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-
-               $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
-                       ? array( 'LOCK IN SHARE MODE' )
-                       : array();
-
-               $id = $db->selectField( 'user',
-                       'user_id', array( 'user_name' => $s ), __METHOD__, $options );
-
-               return (int)$id;
-       }
-
-       /**
-        * Add a user to the database, return the user object
-        *
-        * @param string $name Username to add
-        * @param array $params Array of Strings Non-default parameters to save to
-        *   the database as user_* fields:
-        *   - email: The user's email address.
-        *   - email_authenticated: The email authentication timestamp.
-        *   - real_name: The user's real name.
-        *   - options: An associative array of non-default options.
-        *   - token: Random authentication token. Do not set.
-        *   - registration: Registration timestamp. Do not set.
-        *
-        * @return User|null User object, or null if the username already exists.
-        */
-       public static function createNew( $name, $params = array() ) {
-               foreach ( array( 'password', 'newpassword', 'newpass_time', 'password_expires' ) as $field ) {
-                       if ( isset( $params[$field] ) ) {
-                               wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
-                               unset( $params[$field] );
-                       }
-               }
-
-               $user = new User;
-               $user->load();
-               $user->setToken(); // init token
-               if ( isset( $params['options'] ) ) {
-                       $user->mOptions = $params['options'] + (array)$user->mOptions;
-                       unset( $params['options'] );
-               }
-               $dbw = wfGetDB( DB_MASTER );
-               $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
-
-               $noPass = PasswordFactory::newInvalidPassword()->toString();
-
-               $fields = array(
-                       'user_id' => $seqVal,
-                       'user_name' => $name,
-                       'user_password' => $noPass,
-                       'user_newpassword' => $noPass,
-                       'user_email' => $user->mEmail,
-                       'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
-                       'user_real_name' => $user->mRealName,
-                       'user_token' => strval( $user->mToken ),
-                       'user_registration' => $dbw->timestamp( $user->mRegistration ),
-                       'user_editcount' => 0,
-                       'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
-               );
-               foreach ( $params as $name => $value ) {
-                       $fields["user_$name"] = $value;
-               }
-               $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
-               if ( $dbw->affectedRows() ) {
-                       $newUser = User::newFromId( $dbw->insertId() );
-               } else {
-                       $newUser = null;
-               }
-               return $newUser;
-       }
-
-       /**
-        * Add this existing user object to the database. If the user already
-        * exists, a fatal status object is returned, and the user object is
-        * initialised with the data from the database.
-        *
-        * Previously, this function generated a DB error due to a key conflict
-        * if the user already existed. Many extension callers use this function
-        * in code along the lines of:
-        *
-        *   $user = User::newFromName( $name );
-        *   if ( !$user->isLoggedIn() ) {
-        *       $user->addToDatabase();
-        *   }
-        *   // do something with $user...
-        *
-        * However, this was vulnerable to a race condition (bug 16020). By
-        * initialising the user object if the user exists, we aim to support this
-        * calling sequence as far as possible.
-        *
-        * Note that if the user exists, this function will acquire a write lock,
-        * so it is still advisable to make the call conditional on isLoggedIn(),
-        * and to commit the transaction after calling.
-        *
-        * @throws MWException
-        * @return Status
-        */
-       public function addToDatabase() {
-               $this->load();
-               if ( !$this->mToken ) {
-                       $this->setToken(); // init token
-               }
-
-               $this->mTouched = $this->newTouchedTimestamp();
-
-               $noPass = PasswordFactory::newInvalidPassword()->toString();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $inWrite = $dbw->writesOrCallbacksPending();
-               $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
-               $dbw->insert( 'user',
-                       array(
-                               'user_id' => $seqVal,
-                               'user_name' => $this->mName,
-                               'user_password' => $noPass,
-                               'user_newpassword' => $noPass,
-                               'user_email' => $this->mEmail,
-                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
-                               'user_real_name' => $this->mRealName,
-                               'user_token' => strval( $this->mToken ),
-                               'user_registration' => $dbw->timestamp( $this->mRegistration ),
-                               'user_editcount' => 0,
-                               'user_touched' => $dbw->timestamp( $this->mTouched ),
-                       ), __METHOD__,
-                       array( 'IGNORE' )
-               );
-               if ( !$dbw->affectedRows() ) {
-                       // The queries below cannot happen in the same REPEATABLE-READ snapshot.
-                       // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
-                       if ( $inWrite ) {
-                               // Can't commit due to pending writes that may need atomicity.
-                               // This may cause some lock contention unlike the case below.
-                               $options = array( 'LOCK IN SHARE MODE' );
-                               $flags = self::READ_LOCKING;
-                       } else {
-                               // Often, this case happens early in views before any writes when
-                               // using CentralAuth. It's should be OK to commit and break the snapshot.
-                               $dbw->commit( __METHOD__, 'flush' );
-                               $options = array();
-                               $flags = self::READ_LATEST;
-                       }
-                       $this->mId = $dbw->selectField( 'user', 'user_id',
-                               array( 'user_name' => $this->mName ), __METHOD__, $options );
-                       $loaded = false;
-                       if ( $this->mId ) {
-                               if ( $this->loadFromDatabase( $flags ) ) {
-                                       $loaded = true;
-                               }
-                       }
-                       if ( !$loaded ) {
-                               throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
-                                       "to insert user '{$this->mName}' row, but it was not present in select!" );
-                       }
-                       return Status::newFatal( 'userexists' );
-               }
-               $this->mId = $dbw->insertId();
-               self::$idCacheByName[$this->mName] = $this->mId;
-
-               // Clear instance cache other than user table data, which is already accurate
-               $this->clearInstanceCache();
-
-               $this->saveOptions();
-               return Status::newGood();
-       }
-
-       /**
-        * If this user is logged-in and blocked,
-        * block any IP address they've successfully logged in from.
-        * @return bool A block was spread
-        */
-       public function spreadAnyEditBlock() {
-               if ( $this->isLoggedIn() && $this->isBlocked() ) {
-                       return $this->spreadBlock();
-               }
-               return false;
-       }
-
-       /**
-        * If this (non-anonymous) user is blocked,
-        * block the IP address they've successfully logged in from.
-        * @return bool A block was spread
-        */
-       protected function spreadBlock() {
-               wfDebug( __METHOD__ . "()\n" );
-               $this->load();
-               if ( $this->mId == 0 ) {
-                       return false;
-               }
-
-               $userblock = Block::newFromTarget( $this->getName() );
-               if ( !$userblock ) {
-                       return false;
-               }
-
-               return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
-       }
-
-       /**
-        * Get whether the user is explicitly blocked from account creation.
-        * @return bool|Block
-        */
-       public function isBlockedFromCreateAccount() {
-               $this->getBlockedStatus();
-               if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
-                       return $this->mBlock;
-               }
-
-               # bug 13611: if the IP address the user is trying to create an account from is
-               # blocked with createaccount disabled, prevent new account creation there even
-               # when the user is logged in
-               if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
-                       $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
-               }
-               return $this->mBlockedFromCreateAccount instanceof Block
-                       && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
-                       ? $this->mBlockedFromCreateAccount
-                       : false;
-       }
-
-       /**
-        * Get whether the user is blocked from using Special:Emailuser.
-        * @return bool
-        */
-       public function isBlockedFromEmailuser() {
-               $this->getBlockedStatus();
-               return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
-       }
-
-       /**
-        * Get whether the user is allowed to create an account.
-        * @return bool
-        */
-       public function isAllowedToCreateAccount() {
-               return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
-       }
-
-       /**
-        * Get this user's personal page title.
-        *
-        * @return Title User's personal page title
-        */
-       public function getUserPage() {
-               return Title::makeTitle( NS_USER, $this->getName() );
-       }
-
-       /**
-        * Get this user's talk page title.
-        *
-        * @return Title User's talk page title
-        */
-       public function getTalkPage() {
-               $title = $this->getUserPage();
-               return $title->getTalkPage();
-       }
-
-       /**
-        * Determine whether the user is a newbie. Newbies are either
-        * anonymous IPs, or the most recently created accounts.
-        * @return bool
-        */
-       public function isNewbie() {
-               return !$this->isAllowed( 'autoconfirmed' );
-       }
-
-       /**
-        * Check to see if the given clear-text password is one of the accepted passwords
-        * @deprecated since 1.27. AuthManager is coming.
-        * @param string $password User password
-        * @return bool True if the given password is correct, otherwise False
-        */
-       public function checkPassword( $password ) {
-               global $wgAuth, $wgLegacyEncoding;
-
-               $this->load();
-
-               // Some passwords will give a fatal Status, which means there is
-               // some sort of technical or security reason for this password to
-               // be completely invalid and should never be checked (e.g., T64685)
-               if ( !$this->checkPasswordValidity( $password )->isOK() ) {
-                       return false;
-               }
-
-               // Certain authentication plugins do NOT want to save
-               // domain passwords in a mysql database, so we should
-               // check this (in case $wgAuth->strict() is false).
-               if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
-                       return true;
-               } elseif ( $wgAuth->strict() ) {
-                       // Auth plugin doesn't allow local authentication
-                       return false;
-               } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
-                       // Auth plugin doesn't allow local authentication for this user name
-                       return false;
-               }
-
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-
-               try {
-                       $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
-                               'user', 'user_password', array( 'user_id' => $this->getId() ), __METHOD__
-                       ) );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $mPassword = PasswordFactory::newInvalidPassword();
-               }
-
-               if ( !$mPassword->equals( $password ) ) {
-                       if ( $wgLegacyEncoding ) {
-                               // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
-                               // Check for this with iconv
-                               $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
-                               if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
-                                       return false;
-                               }
-                       } else {
-                               return false;
-                       }
-               }
-
-               if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
-                       $this->setPasswordInternal( $password );
-               }
-
-               return true;
-       }
-
-       /**
-        * Check if the given clear-text password matches the temporary password
-        * sent by e-mail for password reset operations.
-        *
-        * @deprecated since 1.27. AuthManager is coming.
-        * @param string $plaintext
-        * @return bool True if matches, false otherwise
-        */
-       public function checkTemporaryPassword( $plaintext ) {
-               global $wgNewPasswordExpiry;
-
-               $this->load();
-
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-
-               $row = $db->selectRow(
-                       'user',
-                       array( 'user_newpassword', 'user_newpass_time' ),
-                       array( 'user_id' => $this->getId() ),
-                       __METHOD__
-               );
-               try {
-                       $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newPassword = PasswordFactory::newInvalidPassword();
-               }
-
-               if ( $newPassword->equals( $plaintext ) ) {
-                       if ( is_null( $row->user_newpass_time ) ) {
-                               return true;
-                       }
-                       $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
-                       return ( time() < $expiry );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Alias for getEditToken.
-        * @deprecated since 1.19, use getEditToken instead.
-        *
-        * @param string|array $salt Array of Strings Optional function-specific data for hashing
-        * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
-        * @return string The new edit token
-        */
-       public function editToken( $salt = '', $request = null ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               return $this->getEditToken( $salt, $request );
-       }
-
-       /**
-        * Internal implementation for self::getEditToken() and
-        * self::matchEditToken().
-        *
-        * @param string|array $salt
-        * @param WebRequest $request
-        * @param string|int $timestamp
-        * @return string
-        */
-       private function getEditTokenAtTimestamp( $salt, $request, $timestamp ) {
-               if ( $this->isAnon() ) {
-                       return self::EDIT_TOKEN_SUFFIX;
-               } else {
-                       $token = $request->getSessionData( 'wsEditToken' );
-                       if ( $token === null ) {
-                               $token = MWCryptRand::generateHex( 32 );
-                               $request->setSessionData( 'wsEditToken', $token );
-                       }
-                       if ( is_array( $salt ) ) {
-                               $salt = implode( '|', $salt );
-                       }
-                       return hash_hmac( 'md5', $timestamp . $salt, $token, false ) .
-                               dechex( $timestamp ) .
-                               self::EDIT_TOKEN_SUFFIX;
-               }
-       }
-
-       /**
-        * Initialize (if necessary) and return a session token value
-        * which can be used in edit forms to show that the user's
-        * login credentials aren't being hijacked with a foreign form
-        * submission.
-        *
-        * @since 1.19
-        *
-        * @param string|array $salt Array of Strings Optional function-specific data for hashing
-        * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
-        * @return string The new edit token
-        */
-       public function getEditToken( $salt = '', $request = null ) {
-               return $this->getEditTokenAtTimestamp(
-                       $salt, $request ?: $this->getRequest(), wfTimestamp()
-               );
-       }
-
-       /**
-        * Generate a looking random token for various uses.
-        *
-        * @return string The new random token
-        * @deprecated since 1.20: Use MWCryptRand for secure purposes or
-        *   wfRandomString for pseudo-randomness.
-        */
-       public static function generateToken() {
-               return MWCryptRand::generateHex( 32 );
-       }
-
-       /**
-        * Get the embedded timestamp from a token.
-        * @param string $val Input token
-        * @return int|null
-        */
-       public static function getEditTokenTimestamp( $val ) {
-               $suffixLen = strlen( self::EDIT_TOKEN_SUFFIX );
-               if ( strlen( $val ) <= 32 + $suffixLen ) {
-                       return null;
-               }
-
-               return hexdec( substr( $val, 32, -$suffixLen ) );
-       }
-
-       /**
-        * Check given value against the token value stored in the session.
-        * A match should confirm that the form was submitted from the
-        * user's own login session, not a form submission from a third-party
-        * site.
-        *
-        * @param string $val Input value to compare
-        * @param string $salt Optional function-specific data for hashing
-        * @param WebRequest|null $request Object to use or null to use $wgRequest
-        * @param int $maxage Fail tokens older than this, in seconds
-        * @return bool Whether the token matches
-        */
-       public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
-               if ( $this->isAnon() ) {
-                       return $val === self::EDIT_TOKEN_SUFFIX;
-               }
-
-               $timestamp = self::getEditTokenTimestamp( $val );
-               if ( $timestamp === null ) {
-                       return false;
-               }
-               if ( $maxage !== null && $timestamp < wfTimestamp() - $maxage ) {
-                       // Expired token
-                       return false;
-               }
-
-               $sessionToken = $this->getEditTokenAtTimestamp(
-                       $salt, $request ?: $this->getRequest(), $timestamp
-               );
-
-               if ( $val != $sessionToken ) {
-                       wfDebug( "User::matchEditToken: broken session data\n" );
-               }
-
-               return hash_equals( $sessionToken, $val );
-       }
-
-       /**
-        * Check given value against the token value stored in the session,
-        * ignoring the suffix.
-        *
-        * @param string $val Input value to compare
-        * @param string $salt Optional function-specific data for hashing
-        * @param WebRequest|null $request Object to use or null to use $wgRequest
-        * @param int $maxage Fail tokens older than this, in seconds
-        * @return bool Whether the token matches
-        */
-       public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
-               $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . self::EDIT_TOKEN_SUFFIX;
-               return $this->matchEditToken( $val, $salt, $request, $maxage );
-       }
-
-       /**
-        * Generate a new e-mail confirmation token and send a confirmation/invalidation
-        * mail to the user's given address.
-        *
-        * @param string $type Message to send, either "created", "changed" or "set"
-        * @return Status
-        */
-       public function sendConfirmationMail( $type = 'created' ) {
-               global $wgLang;
-               $expiration = null; // gets passed-by-ref and defined in next line.
-               $token = $this->confirmationToken( $expiration );
-               $url = $this->confirmationTokenUrl( $token );
-               $invalidateURL = $this->invalidationTokenUrl( $token );
-               $this->saveSettings();
-
-               if ( $type == 'created' || $type === false ) {
-                       $message = 'confirmemail_body';
-               } elseif ( $type === true ) {
-                       $message = 'confirmemail_body_changed';
-               } else {
-                       // Messages: confirmemail_body_changed, confirmemail_body_set
-                       $message = 'confirmemail_body_' . $type;
-               }
-
-               return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
-                       wfMessage( $message,
-                               $this->getRequest()->getIP(),
-                               $this->getName(),
-                               $url,
-                               $wgLang->timeanddate( $expiration, false ),
-                               $invalidateURL,
-                               $wgLang->date( $expiration, false ),
-                               $wgLang->time( $expiration, false ) )->text() );
-       }
-
-       /**
-        * Send an e-mail to this user's account. Does not check for
-        * confirmed status or validity.
-        *
-        * @param string $subject Message subject
-        * @param string $body Message body
-        * @param User|null $from Optional sending user; if unspecified, default
-        *   $wgPasswordSender will be used.
-        * @param string $replyto Reply-To address
-        * @return Status
-        */
-       public function sendMail( $subject, $body, $from = null, $replyto = null ) {
-               global $wgPasswordSender;
-
-               if ( $from instanceof User ) {
-                       $sender = MailAddress::newFromUser( $from );
-               } else {
-                       $sender = new MailAddress( $wgPasswordSender,
-                               wfMessage( 'emailsender' )->inContentLanguage()->text() );
-               }
-               $to = MailAddress::newFromUser( $this );
-
-               return UserMailer::send( $to, $sender, $subject, $body, array(
-                       'replyTo' => $replyto,
-               ) );
-       }
-
-       /**
-        * Generate, store, and return a new e-mail confirmation code.
-        * A hash (unsalted, since it's used as a key) is stored.
-        *
-        * @note Call saveSettings() after calling this function to commit
-        * this change to the database.
-        *
-        * @param string &$expiration Accepts the expiration time
-        * @return string New token
-        */
-       protected function confirmationToken( &$expiration ) {
-               global $wgUserEmailConfirmationTokenExpiry;
-               $now = time();
-               $expires = $now + $wgUserEmailConfirmationTokenExpiry;
-               $expiration = wfTimestamp( TS_MW, $expires );
-               $this->load();
-               $token = MWCryptRand::generateHex( 32 );
-               $hash = md5( $token );
-               $this->mEmailToken = $hash;
-               $this->mEmailTokenExpires = $expiration;
-               return $token;
-       }
-
-       /**
-        * Return a URL the user can use to confirm their email address.
-        * @param string $token Accepts the email confirmation token
-        * @return string New token URL
-        */
-       protected function confirmationTokenUrl( $token ) {
-               return $this->getTokenUrl( 'ConfirmEmail', $token );
-       }
-
-       /**
-        * Return a URL the user can use to invalidate their email address.
-        * @param string $token Accepts the email confirmation token
-        * @return string New token URL
-        */
-       protected function invalidationTokenUrl( $token ) {
-               return $this->getTokenUrl( 'InvalidateEmail', $token );
-       }
-
-       /**
-        * Internal function to format the e-mail validation/invalidation URLs.
-        * This uses a quickie hack to use the
-        * hardcoded English names of the Special: pages, for ASCII safety.
-        *
-        * @note Since these URLs get dropped directly into emails, using the
-        * short English names avoids insanely long URL-encoded links, which
-        * also sometimes can get corrupted in some browsers/mailers
-        * (bug 6957 with Gmail and Internet Explorer).
-        *
-        * @param string $page Special page
-        * @param string $token Token
-        * @return string Formatted URL
-        */
-       protected function getTokenUrl( $page, $token ) {
-               // Hack to bypass localization of 'Special:'
-               $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
-               return $title->getCanonicalURL();
-       }
-
-       /**
-        * Mark the e-mail address confirmed.
-        *
-        * @note Call saveSettings() after calling this function to commit the change.
-        *
-        * @return bool
-        */
-       public function confirmEmail() {
-               // Check if it's already confirmed, so we don't touch the database
-               // and fire the ConfirmEmailComplete hook on redundant confirmations.
-               if ( !$this->isEmailConfirmed() ) {
-                       $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
-                       Hooks::run( 'ConfirmEmailComplete', array( $this ) );
-               }
-               return true;
-       }
-
-       /**
-        * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
-        * address if it was already confirmed.
-        *
-        * @note Call saveSettings() after calling this function to commit the change.
-        * @return bool Returns true
-        */
-       public function invalidateEmail() {
-               $this->load();
-               $this->mEmailToken = null;
-               $this->mEmailTokenExpires = null;
-               $this->setEmailAuthenticationTimestamp( null );
-               $this->mEmail = '';
-               Hooks::run( 'InvalidateEmailComplete', array( $this ) );
-               return true;
-       }
-
-       /**
-        * Set the e-mail authentication timestamp.
-        * @param string $timestamp TS_MW timestamp
-        */
-       public function setEmailAuthenticationTimestamp( $timestamp ) {
-               $this->load();
-               $this->mEmailAuthenticated = $timestamp;
-               Hooks::run( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
-       }
-
-       /**
-        * Is this user allowed to send e-mails within limits of current
-        * site configuration?
-        * @return bool
-        */
-       public function canSendEmail() {
-               global $wgEnableEmail, $wgEnableUserEmail;
-               if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
-                       return false;
-               }
-               $canSend = $this->isEmailConfirmed();
-               Hooks::run( 'UserCanSendEmail', array( &$this, &$canSend ) );
-               return $canSend;
-       }
-
-       /**
-        * Is this user allowed to receive e-mails within limits of current
-        * site configuration?
-        * @return bool
-        */
-       public function canReceiveEmail() {
-               return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
-       }
-
-       /**
-        * Is this user's e-mail address valid-looking and confirmed within
-        * limits of the current site configuration?
-        *
-        * @note If $wgEmailAuthentication is on, this may require the user to have
-        * confirmed their address by returning a code or using a password
-        * sent to the address from the wiki.
-        *
-        * @return bool
-        */
-       public function isEmailConfirmed() {
-               global $wgEmailAuthentication;
-               $this->load();
-               $confirmed = true;
-               if ( Hooks::run( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
-                       if ( $this->isAnon() ) {
-                               return false;
-                       }
-                       if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
-                               return false;
-                       }
-                       if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
-                               return false;
-                       }
-                       return true;
-               } else {
-                       return $confirmed;
-               }
-       }
-
-       /**
-        * Check whether there is an outstanding request for e-mail confirmation.
-        * @return bool
-        */
-       public function isEmailConfirmationPending() {
-               global $wgEmailAuthentication;
-               return $wgEmailAuthentication &&
-                       !$this->isEmailConfirmed() &&
-                       $this->mEmailToken &&
-                       $this->mEmailTokenExpires > wfTimestamp();
-       }
-
-       /**
-        * Get the timestamp of account creation.
-        *
-        * @return string|bool|null Timestamp of account creation, false for
-        *  non-existent/anonymous user accounts, or null if existing account
-        *  but information is not in database.
-        */
-       public function getRegistration() {
-               if ( $this->isAnon() ) {
-                       return false;
-               }
-               $this->load();
-               return $this->mRegistration;
-       }
-
-       /**
-        * Get the timestamp of the first edit
-        *
-        * @return string|bool Timestamp of first edit, or false for
-        *  non-existent/anonymous user accounts.
-        */
-       public function getFirstEditTimestamp() {
-               if ( $this->getId() == 0 ) {
-                       return false; // anons
-               }
-               $dbr = wfGetDB( DB_SLAVE );
-               $time = $dbr->selectField( 'revision', 'rev_timestamp',
-                       array( 'rev_user' => $this->getId() ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rev_timestamp ASC' )
-               );
-               if ( !$time ) {
-                       return false; // no edits
-               }
-               return wfTimestamp( TS_MW, $time );
-       }
-
-       /**
-        * Get the permissions associated with a given list of groups
-        *
-        * @param array $groups Array of Strings List of internal group names
-        * @return array Array of Strings List of permission key names for given groups combined
-        */
-       public static function getGroupPermissions( $groups ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               $rights = array();
-               // grant every granted permission first
-               foreach ( $groups as $group ) {
-                       if ( isset( $wgGroupPermissions[$group] ) ) {
-                               $rights = array_merge( $rights,
-                                       // array_filter removes empty items
-                                       array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
-                       }
-               }
-               // now revoke the revoked permissions
-               foreach ( $groups as $group ) {
-                       if ( isset( $wgRevokePermissions[$group] ) ) {
-                               $rights = array_diff( $rights,
-                                       array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
-                       }
-               }
-               return array_unique( $rights );
-       }
-
-       /**
-        * Get all the groups who have a given permission
-        *
-        * @param string $role Role to check
-        * @return array Array of Strings List of internal group names with the given permission
-        */
-       public static function getGroupsWithPermission( $role ) {
-               global $wgGroupPermissions;
-               $allowedGroups = array();
-               foreach ( array_keys( $wgGroupPermissions ) as $group ) {
-                       if ( self::groupHasPermission( $group, $role ) ) {
-                               $allowedGroups[] = $group;
-                       }
-               }
-               return $allowedGroups;
-       }
-
-       /**
-        * Check, if the given group has the given permission
-        *
-        * If you're wanting to check whether all users have a permission, use
-        * User::isEveryoneAllowed() instead. That properly checks if it's revoked
-        * from anyone.
-        *
-        * @since 1.21
-        * @param string $group Group to check
-        * @param string $role Role to check
-        * @return bool
-        */
-       public static function groupHasPermission( $group, $role ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
-                       && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
-       }
-
-       /**
-        * Check if all users have the given permission
-        *
-        * @since 1.22
-        * @param string $right Right to check
-        * @return bool
-        */
-       public static function isEveryoneAllowed( $right ) {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               static $cache = array();
-
-               // Use the cached results, except in unit tests which rely on
-               // being able change the permission mid-request
-               if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       return $cache[$right];
-               }
-
-               if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
-                       $cache[$right] = false;
-                       return false;
-               }
-
-               // If it's revoked anywhere, then everyone doesn't have it
-               foreach ( $wgRevokePermissions as $rights ) {
-                       if ( isset( $rights[$right] ) && $rights[$right] ) {
-                               $cache[$right] = false;
-                               return false;
-                       }
-               }
-
-               // Allow extensions (e.g. OAuth) to say false
-               if ( !Hooks::run( 'UserIsEveryoneAllowed', array( $right ) ) ) {
-                       $cache[$right] = false;
-                       return false;
-               }
-
-               $cache[$right] = true;
-               return true;
-       }
-
-       /**
-        * Get the localized descriptive name for a group, if it exists
-        *
-        * @param string $group Internal group name
-        * @return string Localized descriptive group name
-        */
-       public static function getGroupName( $group ) {
-               $msg = wfMessage( "group-$group" );
-               return $msg->isBlank() ? $group : $msg->text();
-       }
-
-       /**
-        * Get the localized descriptive name for a member of a group, if it exists
-        *
-        * @param string $group Internal group name
-        * @param string $username Username for gender (since 1.19)
-        * @return string Localized name for group member
-        */
-       public static function getGroupMember( $group, $username = '#' ) {
-               $msg = wfMessage( "group-$group-member", $username );
-               return $msg->isBlank() ? $group : $msg->text();
-       }
-
-       /**
-        * Return the set of defined explicit groups.
-        * The implicit groups (by default *, 'user' and 'autoconfirmed')
-        * are not included, as they are defined automatically, not in the database.
-        * @return array Array of internal group names
-        */
-       public static function getAllGroups() {
-               global $wgGroupPermissions, $wgRevokePermissions;
-               return array_diff(
-                       array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
-                       self::getImplicitGroups()
-               );
-       }
-
-       /**
-        * Get a list of all available permissions.
-        * @return string[] Array of permission names
-        */
-       public static function getAllRights() {
-               if ( self::$mAllRights === false ) {
-                       global $wgAvailableRights;
-                       if ( count( $wgAvailableRights ) ) {
-                               self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
-                       } else {
-                               self::$mAllRights = self::$mCoreRights;
-                       }
-                       Hooks::run( 'UserGetAllRights', array( &self::$mAllRights ) );
-               }
-               return self::$mAllRights;
-       }
-
-       /**
-        * Get a list of implicit groups
-        * @return array Array of Strings Array of internal group names
-        */
-       public static function getImplicitGroups() {
-               global $wgImplicitGroups;
-
-               $groups = $wgImplicitGroups;
-               # Deprecated, use $wgImplicitGroups instead
-               Hooks::run( 'UserGetImplicitGroups', array( &$groups ), '1.25' );
-
-               return $groups;
-       }
-
-       /**
-        * Get the title of a page describing a particular group
-        *
-        * @param string $group Internal group name
-        * @return Title|bool Title of the page if it exists, false otherwise
-        */
-       public static function getGroupPage( $group ) {
-               $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
-               if ( $msg->exists() ) {
-                       $title = Title::newFromText( $msg->text() );
-                       if ( is_object( $title ) ) {
-                               return $title;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Create a link to the group in HTML, if available;
-        * else return the group name.
-        *
-        * @param string $group Internal name of the group
-        * @param string $text The text of the link
-        * @return string HTML link to the group
-        */
-       public static function makeGroupLinkHTML( $group, $text = '' ) {
-               if ( $text == '' ) {
-                       $text = self::getGroupName( $group );
-               }
-               $title = self::getGroupPage( $group );
-               if ( $title ) {
-                       return Linker::link( $title, htmlspecialchars( $text ) );
-               } else {
-                       return htmlspecialchars( $text );
-               }
-       }
-
-       /**
-        * Create a link to the group in Wikitext, if available;
-        * else return the group name.
-        *
-        * @param string $group Internal name of the group
-        * @param string $text The text of the link
-        * @return string Wikilink to the group
-        */
-       public static function makeGroupLinkWiki( $group, $text = '' ) {
-               if ( $text == '' ) {
-                       $text = self::getGroupName( $group );
-               }
-               $title = self::getGroupPage( $group );
-               if ( $title ) {
-                       $page = $title->getFullText();
-                       return "[[$page|$text]]";
-               } else {
-                       return $text;
-               }
-       }
-
-       /**
-        * Returns an array of the groups that a particular group can add/remove.
-        *
-        * @param string $group The group to check for whether it can add/remove
-        * @return array Array( 'add' => array( addablegroups ),
-        *     'remove' => array( removablegroups ),
-        *     'add-self' => array( addablegroups to self),
-        *     'remove-self' => array( removable groups from self) )
-        */
-       public static function changeableByGroup( $group ) {
-               global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
-
-               $groups = array(
-                       'add' => array(),
-                       'remove' => array(),
-                       'add-self' => array(),
-                       'remove-self' => array()
-               );
-
-               if ( empty( $wgAddGroups[$group] ) ) {
-                       // Don't add anything to $groups
-               } elseif ( $wgAddGroups[$group] === true ) {
-                       // You get everything
-                       $groups['add'] = self::getAllGroups();
-               } elseif ( is_array( $wgAddGroups[$group] ) ) {
-                       $groups['add'] = $wgAddGroups[$group];
-               }
-
-               // Same thing for remove
-               if ( empty( $wgRemoveGroups[$group] ) ) {
-                       // Do nothing
-               } elseif ( $wgRemoveGroups[$group] === true ) {
-                       $groups['remove'] = self::getAllGroups();
-               } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
-                       $groups['remove'] = $wgRemoveGroups[$group];
-               }
-
-               // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
-               if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
-                       foreach ( $wgGroupsAddToSelf as $key => $value ) {
-                               if ( is_int( $key ) ) {
-                                       $wgGroupsAddToSelf['user'][] = $value;
-                               }
-                       }
-               }
-
-               if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
-                       foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
-                               if ( is_int( $key ) ) {
-                                       $wgGroupsRemoveFromSelf['user'][] = $value;
-                               }
-                       }
-               }
-
-               // Now figure out what groups the user can add to him/herself
-               if ( empty( $wgGroupsAddToSelf[$group] ) ) {
-                       // Do nothing
-               } elseif ( $wgGroupsAddToSelf[$group] === true ) {
-                       // No idea WHY this would be used, but it's there
-                       $groups['add-self'] = User::getAllGroups();
-               } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
-                       $groups['add-self'] = $wgGroupsAddToSelf[$group];
-               }
-
-               if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
-                       // Do nothing
-               } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
-                       $groups['remove-self'] = User::getAllGroups();
-               } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
-                       $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
-               }
-
-               return $groups;
-       }
-
-       /**
-        * Returns an array of groups that this user can add and remove
-        * @return array Array( 'add' => array( addablegroups ),
-        *  'remove' => array( removablegroups ),
-        *  'add-self' => array( addablegroups to self),
-        *  'remove-self' => array( removable groups from self) )
-        */
-       public function changeableGroups() {
-               if ( $this->isAllowed( 'userrights' ) ) {
-                       // This group gives the right to modify everything (reverse-
-                       // compatibility with old "userrights lets you change
-                       // everything")
-                       // Using array_merge to make the groups reindexed
-                       $all = array_merge( User::getAllGroups() );
-                       return array(
-                               'add' => $all,
-                               'remove' => $all,
-                               'add-self' => array(),
-                               'remove-self' => array()
-                       );
-               }
-
-               // Okay, it's not so simple, we will have to go through the arrays
-               $groups = array(
-                       'add' => array(),
-                       'remove' => array(),
-                       'add-self' => array(),
-                       'remove-self' => array()
-               );
-               $addergroups = $this->getEffectiveGroups();
-
-               foreach ( $addergroups as $addergroup ) {
-                       $groups = array_merge_recursive(
-                               $groups, $this->changeableByGroup( $addergroup )
-                       );
-                       $groups['add'] = array_unique( $groups['add'] );
-                       $groups['remove'] = array_unique( $groups['remove'] );
-                       $groups['add-self'] = array_unique( $groups['add-self'] );
-                       $groups['remove-self'] = array_unique( $groups['remove-self'] );
-               }
-               return $groups;
-       }
-
-       /**
-        * Deferred version of incEditCountImmediate()
-        */
-       public function incEditCount() {
-               $that = $this;
-               wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $that ) {
-                       $that->incEditCountImmediate();
-               } );
-       }
-
-       /**
-        * Increment the user's edit-count field.
-        * Will have no effect for anonymous users.
-        * @since 1.26
-        */
-       public function incEditCountImmediate() {
-               if ( $this->isAnon() ) {
-                       return;
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               // No rows will be "affected" if user_editcount is NULL
-               $dbw->update(
-                       'user',
-                       array( 'user_editcount=user_editcount+1' ),
-                       array( 'user_id' => $this->getId(), 'user_editcount IS NOT NULL' ),
-                       __METHOD__
-               );
-               // Lazy initialization check...
-               if ( $dbw->affectedRows() == 0 ) {
-                       // Now here's a goddamn hack...
-                       $dbr = wfGetDB( DB_SLAVE );
-                       if ( $dbr !== $dbw ) {
-                               // If we actually have a slave server, the count is
-                               // at least one behind because the current transaction
-                               // has not been committed and replicated.
-                               $this->initEditCount( 1 );
-                       } else {
-                               // But if DB_SLAVE is selecting the master, then the
-                               // count we just read includes the revision that was
-                               // just added in the working transaction.
-                               $this->initEditCount();
-                       }
-               }
-               // Edit count in user cache too
-               $this->invalidateCache();
-       }
-
-       /**
-        * Initialize user_editcount from data out of the revision table
-        *
-        * @param int $add Edits to add to the count from the revision table
-        * @return int Number of edits
-        */
-       protected function initEditCount( $add = 0 ) {
-               // Pull from a slave to be less cruel to servers
-               // Accuracy isn't the point anyway here
-               $dbr = wfGetDB( DB_SLAVE );
-               $count = (int)$dbr->selectField(
-                       'revision',
-                       'COUNT(rev_user)',
-                       array( 'rev_user' => $this->getId() ),
-                       __METHOD__
-               );
-               $count = $count + $add;
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       array( 'user_editcount' => $count ),
-                       array( 'user_id' => $this->getId() ),
-                       __METHOD__
-               );
-
-               return $count;
-       }
-
-       /**
-        * Get the description of a given right
-        *
-        * @param string $right Right to query
-        * @return string Localized description of the right
-        */
-       public static function getRightDescription( $right ) {
-               $key = "right-$right";
-               $msg = wfMessage( $key );
-               return $msg->isBlank() ? $right : $msg->text();
-       }
-
-       /**
-        * Make a new-style password hash
-        *
-        * @param string $password Plain-text password
-        * @param bool|string $salt Optional salt, may be random or the user ID.
-        *  If unspecified or false, will generate one automatically
-        * @return string Password hash
-        * @deprecated since 1.24, use Password class
-        */
-       public static function crypt( $password, $salt = false ) {
-               wfDeprecated( __METHOD__, '1.24' );
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $hash = $passwordFactory->newFromPlaintext( $password );
-               return $hash->toString();
-       }
-
-       /**
-        * Compare a password hash with a plain-text password. Requires the user
-        * ID if there's a chance that the hash is an old-style hash.
-        *
-        * @param string $hash Password hash
-        * @param string $password Plain-text password to compare
-        * @param string|bool $userId User ID for old-style password salt
-        *
-        * @return bool
-        * @deprecated since 1.24, use Password class
-        */
-       public static function comparePasswords( $hash, $password, $userId = false ) {
-               wfDeprecated( __METHOD__, '1.24' );
-
-               // Check for *really* old password hashes that don't even have a type
-               // The old hash format was just an md5 hex hash, with no type information
-               if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
-                       global $wgPasswordSalt;
-                       if ( $wgPasswordSalt ) {
-                               $password = ":B:{$userId}:{$hash}";
-                       } else {
-                               $password = ":A:{$hash}";
-                       }
-               }
-
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $hash = $passwordFactory->newFromCiphertext( $hash );
-               return $hash->equals( $password );
-       }
-
-       /**
-        * Add a newuser log entry for this user.
-        * Before 1.19 the return value was always true.
-        *
-        * @param string|bool $action Account creation type.
-        *   - String, one of the following values:
-        *     - 'create' for an anonymous user creating an account for himself.
-        *       This will force the action's performer to be the created user itself,
-        *       no matter the value of $wgUser
-        *     - 'create2' for a logged in user creating an account for someone else
-        *     - 'byemail' when the created user will receive its password by e-mail
-        *     - 'autocreate' when the user is automatically created (such as by CentralAuth).
-        *   - Boolean means whether the account was created by e-mail (deprecated):
-        *     - true will be converted to 'byemail'
-        *     - false will be converted to 'create' if this object is the same as
-        *       $wgUser and to 'create2' otherwise
-        *
-        * @param string $reason User supplied reason
-        *
-        * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
-        */
-       public function addNewUserLogEntry( $action = false, $reason = '' ) {
-               global $wgUser, $wgNewUserLog;
-               if ( empty( $wgNewUserLog ) ) {
-                       return true; // disabled
-               }
-
-               if ( $action === true ) {
-                       $action = 'byemail';
-               } elseif ( $action === false ) {
-                       if ( $this->equals( $wgUser ) ) {
-                               $action = 'create';
-                       } else {
-                               $action = 'create2';
-                       }
-               }
-
-               if ( $action === 'create' || $action === 'autocreate' ) {
-                       $performer = $this;
-               } else {
-                       $performer = $wgUser;
-               }
-
-               $logEntry = new ManualLogEntry( 'newusers', $action );
-               $logEntry->setPerformer( $performer );
-               $logEntry->setTarget( $this->getUserPage() );
-               $logEntry->setComment( $reason );
-               $logEntry->setParameters( array(
-                       '4::userid' => $this->getId(),
-               ) );
-               $logid = $logEntry->insert();
-
-               if ( $action !== 'autocreate' ) {
-                       $logEntry->publish( $logid );
-               }
-
-               return (int)$logid;
-       }
-
-       /**
-        * Add an autocreate newuser log entry for this user
-        * Used by things like CentralAuth and perhaps other authplugins.
-        * Consider calling addNewUserLogEntry() directly instead.
-        *
-        * @return bool
-        */
-       public function addNewUserLogEntryAutoCreate() {
-               $this->addNewUserLogEntry( 'autocreate' );
-
-               return true;
-       }
-
-       /**
-        * Load the user options either from cache, the database or an array
-        *
-        * @param array $data Rows for the current user out of the user_properties table
-        */
-       protected function loadOptions( $data = null ) {
-               global $wgContLang;
-
-               $this->load();
-
-               if ( $this->mOptionsLoaded ) {
-                       return;
-               }
-
-               $this->mOptions = self::getDefaultOptions();
-
-               if ( !$this->getId() ) {
-                       // For unlogged-in users, load language/variant options from request.
-                       // There's no need to do it for logged-in users: they can set preferences,
-                       // and handling of page content is done by $pageLang->getPreferredVariant() and such,
-                       // so don't override user's choice (especially when the user chooses site default).
-                       $variant = $wgContLang->getDefaultVariant();
-                       $this->mOptions['variant'] = $variant;
-                       $this->mOptions['language'] = $variant;
-                       $this->mOptionsLoaded = true;
-                       return;
-               }
-
-               // Maybe load from the object
-               if ( !is_null( $this->mOptionOverrides ) ) {
-                       wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
-                       foreach ( $this->mOptionOverrides as $key => $value ) {
-                               $this->mOptions[$key] = $value;
-                       }
-               } else {
-                       if ( !is_array( $data ) ) {
-                               wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
-                               // Load from database
-                               $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
-                                       ? wfGetDB( DB_MASTER )
-                                       : wfGetDB( DB_SLAVE );
-
-                               $res = $dbr->select(
-                                       'user_properties',
-                                       array( 'up_property', 'up_value' ),
-                                       array( 'up_user' => $this->getId() ),
-                                       __METHOD__
-                               );
-
-                               $this->mOptionOverrides = array();
-                               $data = array();
-                               foreach ( $res as $row ) {
-                                       $data[$row->up_property] = $row->up_value;
-                               }
-                       }
-                       foreach ( $data as $property => $value ) {
-                               $this->mOptionOverrides[$property] = $value;
-                               $this->mOptions[$property] = $value;
-                       }
-               }
-
-               $this->mOptionsLoaded = true;
-
-               Hooks::run( 'UserLoadOptions', array( $this, &$this->mOptions ) );
-       }
-
-       /**
-        * Saves the non-default options for this user, as previously set e.g. via
-        * setOption(), in the database's "user_properties" (preferences) table.
-        * Usually used via saveSettings().
-        */
-       protected function saveOptions() {
-               $this->loadOptions();
-
-               // Not using getOptions(), to keep hidden preferences in database
-               $saveOptions = $this->mOptions;
-
-               // Allow hooks to abort, for instance to save to a global profile.
-               // Reset options to default state before saving.
-               if ( !Hooks::run( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
-                       return;
-               }
-
-               $userId = $this->getId();
-
-               $insert_rows = array(); // all the new preference rows
-               foreach ( $saveOptions as $key => $value ) {
-                       // Don't bother storing default values
-                       $defaultOption = self::getDefaultOption( $key );
-                       if ( ( $defaultOption === null && $value !== false && $value !== null )
-                               || $value != $defaultOption
-                       ) {
-                               $insert_rows[] = array(
-                                       'up_user' => $userId,
-                                       'up_property' => $key,
-                                       'up_value' => $value,
-                               );
-                       }
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-
-               $res = $dbw->select( 'user_properties',
-                       array( 'up_property', 'up_value' ), array( 'up_user' => $userId ), __METHOD__ );
-
-               // Find prior rows that need to be removed or updated. These rows will
-               // all be deleted (the later so that INSERT IGNORE applies the new values).
-               $keysDelete = array();
-               foreach ( $res as $row ) {
-                       if ( !isset( $saveOptions[$row->up_property] )
-                               || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
-                       ) {
-                               $keysDelete[] = $row->up_property;
-                       }
-               }
-
-               if ( count( $keysDelete ) ) {
-                       // Do the DELETE by PRIMARY KEY for prior rows.
-                       // In the past a very large portion of calls to this function are for setting
-                       // 'rememberpassword' for new accounts (a preference that has since been removed).
-                       // Doing a blanket per-user DELETE for new accounts with no rows in the table
-                       // caused gap locks on [max user ID,+infinity) which caused high contention since
-                       // updates would pile up on each other as they are for higher (newer) user IDs.
-                       // It might not be necessary these days, but it shouldn't hurt either.
-                       $dbw->delete( 'user_properties',
-                               array( 'up_user' => $userId, 'up_property' => $keysDelete ), __METHOD__ );
-               }
-               // Insert the new preference rows
-               $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
-       }
-
-       /**
-        * Lazily instantiate and return a factory object for making passwords
-        *
-        * @deprecated since 1.27, create a PasswordFactory directly instead
-        * @return PasswordFactory
-        */
-       public static function getPasswordFactory() {
-               wfDeprecated( __METHOD__, '1.27' );
-               $ret = new PasswordFactory();
-               $ret->init( RequestContext::getMain()->getConfig() );
-               return $ret;
-       }
-
-       /**
-        * Provide an array of HTML5 attributes to put on an input element
-        * intended for the user to enter a new password.  This may include
-        * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
-        *
-        * Do *not* use this when asking the user to enter his current password!
-        * Regardless of configuration, users may have invalid passwords for whatever
-        * reason (e.g., they were set before requirements were tightened up).
-        * Only use it when asking for a new password, like on account creation or
-        * ResetPass.
-        *
-        * Obviously, you still need to do server-side checking.
-        *
-        * NOTE: A combination of bugs in various browsers means that this function
-        * actually just returns array() unconditionally at the moment.  May as
-        * well keep it around for when the browser bugs get fixed, though.
-        *
-        * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
-        *
-        * @deprecated since 1.27
-        * @return array Array of HTML attributes suitable for feeding to
-        *   Html::element(), directly or indirectly.  (Don't feed to Xml::*()!
-        *   That will get confused by the boolean attribute syntax used.)
-        */
-       public static function passwordChangeInputAttribs() {
-               global $wgMinimalPasswordLength;
-
-               if ( $wgMinimalPasswordLength == 0 ) {
-                       return array();
-               }
-
-               # Note that the pattern requirement will always be satisfied if the
-               # input is empty, so we need required in all cases.
-
-               # @todo FIXME: Bug 23769: This needs to not claim the password is required
-               # if e-mail confirmation is being used.  Since HTML5 input validation
-               # is b0rked anyway in some browsers, just return nothing.  When it's
-               # re-enabled, fix this code to not output required for e-mail
-               # registration.
-               # $ret = array( 'required' );
-               $ret = array();
-
-               # We can't actually do this right now, because Opera 9.6 will print out
-               # the entered password visibly in its error message!  When other
-               # browsers add support for this attribute, or Opera fixes its support,
-               # we can add support with a version check to avoid doing this on Opera
-               # versions where it will be a problem.  Reported to Opera as
-               # DSK-262266, but they don't have a public bug tracker for us to follow.
-               /*
-               if ( $wgMinimalPasswordLength > 1 ) {
-                       $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
-                       $ret['title'] = wfMessage( 'passwordtooshort' )
-                               ->numParams( $wgMinimalPasswordLength )->text();
-               }
-               */
-
-               return $ret;
-       }
-
-       /**
-        * Return the list of user fields that should be selected to create
-        * a new user object.
-        * @return array
-        */
-       public static function selectFields() {
-               return array(
-                       'user_id',
-                       'user_name',
-                       'user_real_name',
-                       'user_email',
-                       'user_touched',
-                       'user_token',
-                       'user_email_authenticated',
-                       'user_email_token',
-                       'user_email_token_expires',
-                       'user_registration',
-                       'user_editcount',
-               );
-       }
-
-       /**
-        * Factory function for fatal permission-denied errors
-        *
-        * @since 1.22
-        * @param string $permission User right required
-        * @return Status
-        */
-       static function newFatalPermissionDeniedStatus( $permission ) {
-               global $wgLang;
-
-               $groups = array_map(
-                       array( 'User', 'makeGroupLinkWiki' ),
-                       User::getGroupsWithPermission( $permission )
-               );
-
-               if ( $groups ) {
-                       return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
-               } else {
-                       return Status::newFatal( 'badaccess-group0' );
-               }
-       }
-
-       /**
-        * Checks if two user objects point to the same user.
-        *
-        * @since 1.25
-        * @param User $user
-        * @return bool
-        */
-       public function equals( User $user ) {
-               return $this->getName() === $user->getName();
-       }
-}
diff --git a/includes/UserArray.php b/includes/UserArray.php
deleted file mode 100644 (file)
index 31bd601..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * Class to walk into a list of User objects.
- *
- * 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
- */
-
-abstract class UserArray implements Iterator {
-       /**
-        * @param ResultWrapper $res
-        * @return UserArrayFromResult
-        */
-       static function newFromResult( $res ) {
-               $userArray = null;
-               if ( !Hooks::run( 'UserArrayFromResult', array( &$userArray, $res ) ) ) {
-                       return null;
-               }
-               if ( $userArray === null ) {
-                       $userArray = self::newFromResult_internal( $res );
-               }
-               return $userArray;
-       }
-
-       /**
-        * @param array $ids
-        * @return UserArrayFromResult
-        */
-       static function newFromIDs( $ids ) {
-               $ids = array_map( 'intval', (array)$ids ); // paranoia
-               if ( !$ids ) {
-                       // Database::select() doesn't like empty arrays
-                       return new ArrayIterator( array() );
-               }
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select(
-                       'user',
-                       User::selectFields(),
-                       array( 'user_id' => array_unique( $ids ) ),
-                       __METHOD__
-               );
-               return self::newFromResult( $res );
-       }
-
-       /**
-        * @since 1.25
-        * @param array $names
-        * @return UserArrayFromResult
-        */
-       static function newFromNames( $names ) {
-               $names = array_map( 'strval', (array)$names ); // paranoia
-               if ( !$names ) {
-                       // Database::select() doesn't like empty arrays
-                       return new ArrayIterator( array() );
-               }
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select(
-                       'user',
-                       User::selectFields(),
-                       array( 'user_name' => array_unique( $names ) ),
-                       __METHOD__
-               );
-               return self::newFromResult( $res );
-       }
-
-       /**
-        * @param ResultWrapper $res
-        * @return UserArrayFromResult
-        */
-       protected static function newFromResult_internal( $res ) {
-               return new UserArrayFromResult( $res );
-       }
-}
diff --git a/includes/UserArrayFromResult.php b/includes/UserArrayFromResult.php
deleted file mode 100644 (file)
index fb533d0..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-/**
- * Class to walk into a list of User objects.
- *
- * 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
- */
-
-class UserArrayFromResult extends UserArray implements Countable {
-       /** @var ResultWrapper */
-       public $res;
-
-       /** @var int */
-       public $key;
-
-       /** @var bool|stdClass */
-       public $current;
-
-       /**
-        * @param ResultWrapper $res
-        */
-       function __construct( $res ) {
-               $this->res = $res;
-               $this->key = 0;
-               $this->setCurrent( $this->res->current() );
-       }
-
-       /**
-        * @param bool|stdClass $row
-        * @return void
-        */
-       protected function setCurrent( $row ) {
-               if ( $row === false ) {
-                       $this->current = false;
-               } else {
-                       $this->current = User::newFromRow( $row );
-               }
-       }
-
-       /**
-        * @return int
-        */
-       public function count() {
-               return $this->res->numRows();
-       }
-
-       /**
-        * @return User
-        */
-       function current() {
-               return $this->current;
-       }
-
-       function key() {
-               return $this->key;
-       }
-
-       function next() {
-               $row = $this->res->next();
-               $this->setCurrent( $row );
-               $this->key++;
-       }
-
-       function rewind() {
-               $this->res->rewind();
-               $this->key = 0;
-               $this->setCurrent( $this->res->current() );
-       }
-
-       /**
-        * @return bool
-        */
-       function valid() {
-               return $this->current !== false;
-       }
-}
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
deleted file mode 100644 (file)
index e686ae3..0000000
+++ /dev/null
@@ -1,287 +0,0 @@
-<?php
-/**
- * Representation of an user on a other locally-hosted wiki.
- *
- * 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
- */
-
-/**
- * Cut-down copy of User interface for local-interwiki-database
- * user rights manipulation.
- */
-class UserRightsProxy {
-
-       /**
-        * Constructor.
-        *
-        * @see newFromId()
-        * @see newFromName()
-        * @param IDatabase $db Db connection
-        * @param string $database Database name
-        * @param string $name User name
-        * @param int $id User ID
-        */
-       private function __construct( $db, $database, $name, $id ) {
-               $this->db = $db;
-               $this->database = $database;
-               $this->name = $name;
-               $this->id = intval( $id );
-               $this->newOptions = array();
-       }
-
-       /**
-        * Accessor for $this->database
-        *
-        * @return string Database name
-        */
-       public function getDBName() {
-               return $this->database;
-       }
-
-       /**
-        * Confirm the selected database name is a valid local interwiki database name.
-        *
-        * @param string $database Database name
-        * @return bool
-        */
-       public static function validDatabase( $database ) {
-               global $wgLocalDatabases;
-               return in_array( $database, $wgLocalDatabases );
-       }
-
-       /**
-        * Same as User::whoIs()
-        *
-        * @param string $database Database name
-        * @param int $id User ID
-        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
-        * @return string User name or false if the user doesn't exist
-        */
-       public static function whoIs( $database, $id, $ignoreInvalidDB = false ) {
-               $user = self::newFromId( $database, $id, $ignoreInvalidDB );
-               if ( $user ) {
-                       return $user->name;
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Factory function; get a remote user entry by ID number.
-        *
-        * @param string $database Database name
-        * @param int $id User ID
-        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
-        * @return UserRightsProxy|null If doesn't exist
-        */
-       public static function newFromId( $database, $id, $ignoreInvalidDB = false ) {
-               return self::newFromLookup( $database, 'user_id', intval( $id ), $ignoreInvalidDB );
-       }
-
-       /**
-        * Factory function; get a remote user entry by name.
-        *
-        * @param string $database Database name
-        * @param string $name User name
-        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
-        * @return UserRightsProxy|null If doesn't exist
-        */
-       public static function newFromName( $database, $name, $ignoreInvalidDB = false ) {
-               return self::newFromLookup( $database, 'user_name', $name, $ignoreInvalidDB );
-       }
-
-       /**
-        * @param string $database
-        * @param string $field
-        * @param string $value
-        * @param bool $ignoreInvalidDB
-        * @return null|UserRightsProxy
-        */
-       private static function newFromLookup( $database, $field, $value, $ignoreInvalidDB = false ) {
-               global $wgSharedDB, $wgSharedTables;
-               // If the user table is shared, perform the user query on it,
-               // but don't pass it to the UserRightsProxy,
-               // as user rights are normally not shared.
-               if ( $wgSharedDB && in_array( 'user', $wgSharedTables ) ) {
-                       $userdb = self::getDB( $wgSharedDB, $ignoreInvalidDB );
-               } else {
-                       $userdb = self::getDB( $database, $ignoreInvalidDB );
-               }
-
-               $db = self::getDB( $database, $ignoreInvalidDB );
-
-               if ( $db && $userdb ) {
-                       $row = $userdb->selectRow( 'user',
-                               array( 'user_id', 'user_name' ),
-                               array( $field => $value ),
-                               __METHOD__ );
-
-                       if ( $row !== false ) {
-                               return new UserRightsProxy( $db, $database,
-                                       $row->user_name,
-                                       intval( $row->user_id ) );
-                       }
-               }
-               return null;
-       }
-
-       /**
-        * Open a database connection to work on for the requested user.
-        * This may be a new connection to another database for remote users.
-        *
-        * @param string $database
-        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
-        * @return IDatabase|null If invalid selection
-        */
-       public static function getDB( $database, $ignoreInvalidDB = false ) {
-               global $wgDBname;
-               if ( $ignoreInvalidDB || self::validDatabase( $database ) ) {
-                       if ( $database == $wgDBname ) {
-                               // Hmm... this shouldn't happen though. :)
-                               return wfGetDB( DB_MASTER );
-                       } else {
-                               return wfGetDB( DB_MASTER, array(), $database );
-                       }
-               }
-               return null;
-       }
-
-       /**
-        * @return int
-        */
-       public function getId() {
-               return $this->id;
-       }
-
-       /**
-        * @return bool
-        */
-       public function isAnon() {
-               return $this->getId() == 0;
-       }
-
-       /**
-        * Same as User::getName()
-        *
-        * @return string
-        */
-       public function getName() {
-               return $this->name . '@' . $this->database;
-       }
-
-       /**
-        * Same as User::getUserPage()
-        *
-        * @return Title
-        */
-       public function getUserPage() {
-               return Title::makeTitle( NS_USER, $this->getName() );
-       }
-
-       /**
-        * Replaces User::getUserGroups()
-        * @return array
-        */
-       function getGroups() {
-               $res = $this->db->select( 'user_groups',
-                       array( 'ug_group' ),
-                       array( 'ug_user' => $this->id ),
-                       __METHOD__ );
-               $groups = array();
-               foreach ( $res as $row ) {
-                       $groups[] = $row->ug_group;
-               }
-               return $groups;
-       }
-
-       /**
-        * Replaces User::addUserGroup()
-        * @param string $group
-        *
-        * @return bool
-        */
-       function addGroup( $group ) {
-               $this->db->insert( 'user_groups',
-                       array(
-                               'ug_user' => $this->id,
-                               'ug_group' => $group,
-                       ),
-                       __METHOD__,
-                       array( 'IGNORE' ) );
-
-               return true;
-       }
-
-       /**
-        * Replaces User::removeUserGroup()
-        * @param string $group
-        *
-        * @return bool
-        */
-       function removeGroup( $group ) {
-               $this->db->delete( 'user_groups',
-                       array(
-                               'ug_user' => $this->id,
-                               'ug_group' => $group,
-                       ),
-                       __METHOD__ );
-
-               return true;
-       }
-
-       /**
-        * Replaces User::setOption()
-        * @param string $option
-        * @param mixed $value
-        */
-       public function setOption( $option, $value ) {
-               $this->newOptions[$option] = $value;
-       }
-
-       public function saveSettings() {
-               $rows = array();
-               foreach ( $this->newOptions as $option => $value ) {
-                       $rows[] = array(
-                               'up_user' => $this->id,
-                               'up_property' => $option,
-                               'up_value' => $value,
-                       );
-               }
-               $this->db->replace( 'user_properties',
-                       array( array( 'up_user', 'up_property' ) ),
-                       $rows, __METHOD__
-               );
-               $this->invalidateCache();
-       }
-
-       /**
-        * Replaces User::touchUser()
-        */
-       function invalidateCache() {
-               $this->db->update( 'user',
-                       array( 'user_touched' => $this->db->timestamp() ),
-                       array( 'user_id' => $this->id ),
-                       __METHOD__ );
-
-               $wikiId = $this->db->getWikiID();
-               $userId = $this->id;
-               $this->db->onTransactionPreCommitOrIdle( function() use ( $wikiId, $userId ) {
-                       User::purge( $wikiId, $userId );
-               } );
-       }
-}
index 8a63070..72adaf8 100644 (file)
@@ -115,7 +115,7 @@ class ApiExpandTemplates extends ApiBase {
                                                foreach ( $categories as $category => $sortkey ) {
                                                        $entry = array();
                                                        $entry['sortkey'] = $sortkey;
-                                                       ApiResult::setContentValue( $entry, 'category', $category );
+                                                       ApiResult::setContentValue( $entry, 'category', (string)$category );
                                                        $categories_result[] = $entry;
                                                }
                                                ApiResult::setIndexedTagName( $categories_result, 'category' );
index 77a3a21..c841d83 100644 (file)
@@ -265,7 +265,9 @@ class ApiFeedWatchlist extends ApiBase {
                                if ( !isset( $p[ApiBase::PARAM_HELP_MSG] ) ) {
                                        $p[ApiBase::PARAM_HELP_MSG] = "apihelp-query+watchlist-param-$from";
                                }
-                               if ( is_array( $p[ApiBase::PARAM_TYPE] ) && isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
+                               if ( isset( $p[ApiBase::PARAM_TYPE] ) && is_array( $p[ApiBase::PARAM_TYPE] ) &&
+                                       isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE] )
+                               ) {
                                        foreach ( $p[ApiBase::PARAM_TYPE] as $v ) {
                                                if ( !isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] ) ) {
                                                        $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] = "apihelp-query+watchlist-paramvalue-$from-$v";
index 9de5a02..5754c23 100644 (file)
@@ -634,7 +634,8 @@ class ApiParse extends ApiBase {
                foreach ( $links as $link => $sortkey ) {
                        $entry = array();
                        $entry['sortkey'] = $sortkey;
-                       ApiResult::setContentValue( $entry, 'category', $link );
+                       // array keys will cast numeric category names to ints, so cast back to string
+                       ApiResult::setContentValue( $entry, 'category', (string)$link );
                        if ( !isset( $hiddencats[$link] ) ) {
                                $entry['missing'] = true;
                        } elseif ( $hiddencats[$link] ) {
index ffcb2f5..9ea1b1e 100644 (file)
@@ -59,9 +59,10 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        $fld_rights = isset( $prop['rights'] );
                        $fld_registration = isset( $prop['registration'] );
                        $fld_implicitgroups = isset( $prop['implicitgroups'] );
+                       $fld_centralids = isset( $prop['centralids'] );
                } else {
                        $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration =
-                               $fld_rights = $fld_implicitgroups = false;
+                               $fld_rights = $fld_implicitgroups = $fld_centralids = false;
                }
 
                $limit = $params['limit'];
@@ -239,6 +240,12 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                'name' => $row->user_name,
                        );
 
+                       if ( $fld_centralids ) {
+                               $data += ApiQueryUserInfo::getCentralUserInfo(
+                                       $this->getConfig(), User::newFromId( $row->user_id ), $params['attachedwiki']
+                               );
+                       }
+
                        if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) {
                                $data['blockid'] = (int)$row->ipb_id;
                                $data['blockedby'] = $row->ipb_by_text;
@@ -338,7 +345,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                        'implicitgroups',
                                        'rights',
                                        'editcount',
-                                       'registration'
+                                       'registration',
+                                       'centralids',
                                ),
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => array(),
                        ),
@@ -357,6 +365,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                        $this->getConfig()->get( 'ActiveUserDays' )
                                ),
                        ),
+                       'attachedwiki' => null,
                );
        }
 
index 7047339..1c031ff 100644 (file)
@@ -265,6 +265,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        $data['favicon'] = wfExpandUrl( $favicon, PROTO_RELATIVE );
                }
 
+               $data['centralidlookupprovider'] = $this->getConfig()->get( 'CentralIdLookupProvider' );
+               $providerIds = array_keys( $this->getConfig()->get( 'CentralIdLookupProviders' ) );
+               $data['allcentralidlookupproviders'] = $providerIds;
+
                Hooks::run( 'APIQuerySiteInfoGeneralInfo', array( $this, &$data ) );
 
                return $this->getResult()->addValue( 'query', $property, $data );
index 93c0dd0..27663a1 100644 (file)
@@ -33,6 +33,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
 
        const WL_UNREAD_LIMIT = 1000;
 
+       private $params = array();
        private $prop = array();
 
        public function __construct( ApiQuery $query, $moduleName ) {
@@ -40,11 +41,11 @@ class ApiQueryUserInfo extends ApiQueryBase {
        }
 
        public function execute() {
-               $params = $this->extractRequestParams();
+               $this->params = $this->extractRequestParams();
                $result = $this->getResult();
 
-               if ( !is_null( $params['prop'] ) ) {
-                       $this->prop = array_flip( $params['prop'] );
+               if ( !is_null( $this->params['prop'] ) ) {
+                       $this->prop = array_flip( $this->params['prop'] );
                }
 
                $r = $this->getCurrentUserInfo();
@@ -76,6 +77,45 @@ class ApiQueryUserInfo extends ApiQueryBase {
                return $vals;
        }
 
+       /**
+        * Get central user info
+        * @param Config $config
+        * @param User $user
+        * @param string|null $attachedWiki
+        * @return array Central user info
+        *  - centralids: Array mapping non-local Central ID provider names to IDs
+        *  - attachedlocal: Array mapping Central ID provider names to booleans
+        *    indicating whether the local user is attached.
+        *  - attachedwiki: Array mapping Central ID provider names to booleans
+        *    indicating whether the user is attached to $attachedWiki.
+        */
+       public static function getCentralUserInfo( Config $config, User $user, $attachedWiki = null ) {
+               $providerIds = array_keys( $config->get( 'CentralIdLookupProviders' ) );
+
+               $ret = array(
+                       'centralids' => array(),
+                       'attachedlocal' => array(),
+               );
+               ApiResult::setArrayType( $ret['centralids'], 'assoc' );
+               ApiResult::setArrayType( $ret['attachedlocal'], 'assoc' );
+               if ( $attachedWiki ) {
+                       $ret['attachedwiki'] = array();
+                       ApiResult::setArrayType( $ret['attachedwiki'], 'assoc' );
+               }
+
+               $name = $user->getName();
+               foreach ( $providerIds as $providerId ) {
+                       $provider = CentralIdLookup::factory( $providerId );
+                       $ret['centralids'][$providerId] = $provider->centralIdFromName( $name );
+                       $ret['attachedlocal'][$providerId] = $provider->isAttached( $user );
+                       if ( $attachedWiki ) {
+                               $ret['attachedwiki'][$providerId] = $provider->isAttached( $user, $attachedWiki );
+                       }
+               }
+
+               return $ret;
+       }
+
        protected function getCurrentUserInfo() {
                $user = $this->getUser();
                $vals = array();
@@ -205,6 +245,12 @@ class ApiQueryUserInfo extends ApiQueryBase {
                        }
                }
 
+               if ( isset( $this->prop['centralids'] ) ) {
+                       $vals += self::getCentralUserInfo(
+                               $this->getConfig(), $this->getUser(), $this->params['attachedwiki']
+                       );
+               }
+
                return $vals;
        }
 
@@ -267,6 +313,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
                                        'acceptlang',
                                        'registrationdate',
                                        'unreadcount',
+                                       'centralids',
                                ),
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => array(
                                        'unreadcount' => array(
@@ -275,7 +322,8 @@ class ApiQueryUserInfo extends ApiQueryBase {
                                                self::WL_UNREAD_LIMIT . '+',
                                        ),
                                ),
-                       )
+                       ),
+                       'attachedwiki' => null,
                );
        }
 
index a826c1b..db5fb65 100644 (file)
@@ -48,6 +48,7 @@ class ApiQueryUsers extends ApiQueryBase {
                'registration',
                'emailable',
                'gender',
+               'centralids',
        );
 
        public function __construct( ApiQuery $query, $moduleName ) {
@@ -213,6 +214,12 @@ class ApiQueryUsers extends ApiQueryBase {
                                        $data[$name]['gender'] = $gender;
                                }
 
+                               if ( isset( $this->prop['centralids'] ) ) {
+                                       $data[$name] += ApiQueryUserInfo::getCentralUserInfo(
+                                               $this->getConfig(), $user, $params['attachedwiki']
+                                       );
+                               }
+
                                if ( !is_null( $params['token'] ) ) {
                                        $tokenFunctions = $this->getTokenFunctions();
                                        foreach ( $params['token'] as $t ) {
@@ -304,9 +311,11 @@ class ApiQueryUsers extends ApiQueryBase {
                                        'registration',
                                        'emailable',
                                        'gender',
+                                       'centralids',
                                ),
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => array(),
                        ),
+                       'attachedwiki' => null,
                        'users' => array(
                                ApiBase::PARAM_ISMULTI => true
                        ),
index c188d15..55b5901 100644 (file)
        "apihelp-query+prefixsearch-param-search": "Such-Zeichenfolge.",
        "apihelp-query+recentchanges-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel für die Bearbeitung.",
        "apihelp-query+recentchanges-paramvalue-prop-tags": "Listet Markierungen für den Eintrag auf.",
+       "apihelp-query+recentchanges-example-simple": "Listet die letzten Änderungen auf.",
+       "apihelp-query+redirects-paramvalue-prop-pageid": "Seitenkennung einer jeden Weiterleitung.",
+       "apihelp-query+redirects-paramvalue-prop-title": "Titel einer jeden Weiterleitung.",
+       "apihelp-query+revisions+base-paramvalue-prop-ids": "Die Kennung der Version.",
+       "apihelp-query+revisions+base-paramvalue-prop-timestamp": "Der Zeitstempel der Version.",
+       "apihelp-query+revisions+base-paramvalue-prop-size": "Länge in Bytes der Version.",
        "apihelp-query+revisions+base-paramvalue-prop-content": "Text der Version.",
        "apihelp-query+search-param-prop": "Eigenschaften zur Rückgabe:",
        "apihelp-query+search-example-simple": "Nach <kbd>meaning</kbd> suchen.",
index 033904d..99a0f42 100644 (file)
@@ -6,10 +6,27 @@
        },
        "apihelp-createaccount-param-name": "Nameyê karberi.",
        "apihelp-delete-description": "Pele bestere.",
+       "apihelp-disabled-description": "Eno modul aktiv niyo.",
+       "apihelp-edit-description": "Vırazı u pela bıvurne",
        "apihelp-edit-param-text": "Zerreki pele",
+       "apihelp-edit-param-minor": "Vurnayışo qıckek.",
+       "apihelp-edit-param-notminor": "Vurnayışo qıckek niyo.",
+       "apihelp-edit-param-bot": "Ney vurnayışa boti nışan ke",
        "apihelp-edit-example-edit": "Şeker bıvurne",
+       "apihelp-emailuser-description": "E-postaya karberi",
+       "apihelp-emailuser-param-target": "Karbero ke cırê eposta do bırışiyo",
+       "apihelp-emailuser-param-subject": "Sernamey mewzu",
+       "apihelp-emailuser-param-text": "Metna eposta",
+       "apihelp-emailuser-param-ccme": "Ju kopyay ena posta bırış mırê.",
+       "apihelp-expandtemplates-param-title": "Sernemay peler",
+       "apihelp-expandtemplates-param-text": "Wikimetini açarnê",
+       "apihelp-feedrecentchanges-param-hideminor": "Vurnayışanê qıckan bınımne",
+       "apihelp-feedrecentchanges-param-hidebots": "Vurnayışanê botan bınımne.",
+       "apihelp-feedrecentchanges-param-hideanons": "Vurnayışané karberan de anoniman bınımne",
+       "apihelp-feedrecentchanges-param-hideliu": "Vurnayışanê karberan de qeydınan bınımne.",
        "apihelp-login-param-name": "Nameyê karberi.",
        "apihelp-login-param-password": "Parola.",
        "apihelp-login-param-domain": "Domain (optional).",
-       "apihelp-login-example-login": "Dekew."
+       "apihelp-login-example-login": "Dekew.",
+       "apihelp-move-description": "Peler bere."
 }
index 4c2d0be..897c05e 100644 (file)
        "apihelp-query+allusers-paramvalue-prop-rights": "Lists rights that the user has.",
        "apihelp-query+allusers-paramvalue-prop-editcount": "Adds the edit count of the user.",
        "apihelp-query+allusers-paramvalue-prop-registration": "Adds the timestamp of when the user registered if available (may be blank).",
+       "apihelp-query+allusers-paramvalue-prop-centralids": "Adds the central IDs and attachment status for the user.",
        "apihelp-query+allusers-param-limit": "How many total user names to return.",
        "apihelp-query+allusers-param-witheditsonly": "Only list users who have made edits.",
        "apihelp-query+allusers-param-activeusers": "Only list users active in the last $1 {{PLURAL:$1|day|days}}.",
+       "apihelp-query+allusers-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, also indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+allusers-example-Y": "List users starting at <kbd>Y</kbd>.",
 
        "apihelp-query+backlinks-description": "Find all pages that link to the given page.",
        "apihelp-query+userinfo-paramvalue-prop-acceptlang": "Echoes the <code>Accept-Language</code> header sent by the client in a structured format.",
        "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Adds the user's registration date.",
        "apihelp-query+userinfo-paramvalue-prop-unreadcount": "Adds the count of unread pages on the user's watchlist (maximum $1; returns <samp>$2</samp> if more).",
+       "apihelp-query+userinfo-paramvalue-prop-centralids": "Adds the central IDs and attachment status for the user.",
+       "apihelp-query+userinfo-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+userinfo-example-simple": "Get information about the current user.",
        "apihelp-query+userinfo-example-data": "Get additional information about the current user.",
 
        "apihelp-query+users-paramvalue-prop-registration": "Adds the user's registration timestamp.",
        "apihelp-query+users-paramvalue-prop-emailable": "Tags if the user can and wants to receive email through [[Special:Emailuser]].",
        "apihelp-query+users-paramvalue-prop-gender": "Tags the gender of the user. Returns \"male\", \"female\", or \"unknown\".",
+       "apihelp-query+users-paramvalue-prop-centralids": "Adds the central IDs and attachment status for the user.",
+       "apihelp-query+users-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+users-param-users": "A list of users to obtain information for.",
        "apihelp-query+users-param-token": "Use <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> instead.",
        "apihelp-query+users-example-simple": "Return information for user <kbd>Example</kbd>.",
index 294a7e9..079eeb2 100644 (file)
@@ -2,9 +2,11 @@
        "@metadata": {
                "authors": [
                        "Subi",
-                       "Sator"
+                       "Sator",
+                       "An13sa"
                ]
        },
+       "apihelp-main-param-action": "Zein ekintza burutuko da.",
        "apihelp-main-param-format": "Irteerako formatua.",
        "apihelp-block-description": "Blokeatu erabiltzaile bat.",
        "apihelp-block-param-reason": "Blokeatzeko arrazoia.",
@@ -14,7 +16,9 @@
        "apihelp-createaccount-param-realname": "Erabiltzailearen benetako izena (aukerakoa).",
        "apihelp-delete-description": "Orrialde bat ezabatu.",
        "apihelp-delete-example-simple": "Ezabatu <kbd>Main Page</kbd>.",
+       "apihelp-disabled-description": "Modulu hau ezgaitu da.",
        "apihelp-edit-description": "Orrialdeak sortu eta aldatu.",
+       "apihelp-edit-param-sectiontitle": "Atal berri baten titulua.",
        "apihelp-edit-param-text": "Orrialdearen edukia.",
        "apihelp-edit-param-minor": "Aldaketa txikia.",
        "apihelp-edit-example-edit": "Orrialde bat aldatu",
@@ -22,6 +26,7 @@
        "apihelp-emailuser-param-subject": "Gaiaren goiburua.",
        "apihelp-emailuser-param-text": "Mezuaren gorputza.",
        "apihelp-expandtemplates-param-title": "Orrialdearen izenburua.",
+       "apihelp-expandtemplates-paramvalue-prop-wikitext": "Wikitestu zabaldua.",
        "apihelp-feedcontributions-param-year": "Urtetik aurrera (eta lehenagotik)",
        "apihelp-feedcontributions-param-month": "Hilabetetik aurrera (eta lehenagotik)",
        "apihelp-feedrecentchanges-param-hideminor": "Ezkutatu aldaketa txikiak.",
        "apihelp-feedrecentchanges-example-simple": "Erakutsi aldaketa berriak",
        "apihelp-feedrecentchanges-example-30days": "Erakutsi aldaketa berriak 30 egunez",
        "apihelp-filerevert-param-comment": "Iruzkina igo.",
+       "apihelp-help-example-recursive": "Laguntza guztia orrialde batean.",
        "apihelp-imagerotate-description": "Irudi bat edo gehiago biratu.",
+       "apihelp-import-param-summary": "Inportazioaren laburpena.",
+       "apihelp-import-param-xml": "XML fitxategia igo da.",
        "apihelp-login-param-name": "Erabiltzaile izena.",
        "apihelp-login-param-password": "Pasahitza.",
        "apihelp-login-param-domain": "Domeinua (hautazkoa).",
        "apihelp-login-example-login": "Saioa hasi",
        "apihelp-move-description": "Orrialde bat mugitu",
        "apihelp-move-param-reason": "Berrizenpenaren arrazoia.",
+       "apihelp-move-param-noredirect": "Birzuzenketarik ez sortu.",
+       "apihelp-move-param-ignorewarnings": "Edozein ohar ezikusi.",
+       "apihelp-opensearch-param-namespace": "Bilatzeko izen-tarteak.",
+       "apihelp-opensearch-param-format": "Irteerako formatua.",
        "apihelp-options-example-reset": "Berrezarri hobespen guztiak.",
+       "apihelp-paraminfo-description": "API moduluei buruzko informazioa eskuratu.",
+       "apihelp-parse-param-summary": "Analizatzeko laburpena.",
        "apihelp-protect-param-reason": "Babesteko edo babesa kentzeko zergatia.",
        "apihelp-protect-example-protect": "Orrialde bat babestu",
+       "apihelp-query+allcategories-description": "Kategoria guztiak zenbakitu.",
        "apihelp-query+allusers-param-witheditsonly": "Bakarrik zerrendatu aldaketak egin dituzten erabiltzaileak.",
        "apihelp-query+allusers-param-activeusers": "Bakarrik zerrendatu azken {{PLURAL:$1|eguneko|$1 egunetako}} erabiltzaile aktiboak.",
        "apihelp-query+blocks-description": "Zerrendatu blokeatutako erabiltzaile eta IP helbide guztiak.",
index 1518bcb..d306c3e 100644 (file)
        "apihelp-query+allusers-paramvalue-prop-rights": "Liste les droits qu’à l’utilisateur.",
        "apihelp-query+allusers-paramvalue-prop-editcount": "Ajoute le compteur de modifications de l’utilisateur.",
        "apihelp-query+allusers-paramvalue-prop-registration": "Ajoute l’horodatage de l’inscription de l’utilisateur, s’il est disponible (peut être vide).",
+       "apihelp-query+allusers-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.",
        "apihelp-query+allusers-param-limit": "Combien de noms d’utilisateur renvoyer au total.",
        "apihelp-query+allusers-param-witheditsonly": "Ne lister que les utilisateurs qui ont fait des modifications.",
        "apihelp-query+allusers-param-activeusers": "Lister uniquement les utilisateurs actifs durant {{PLURAL:$1|le dernier jour|les $1 derniers jours}}.",
+       "apihelp-query+allusers-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer aussi si l’utilisateur est attaché avec le wiki identifié par cet ID.",
        "apihelp-query+allusers-example-Y": "Lister les utilisateurs en commençant à <kbd>Y</kbd>.",
        "apihelp-query+backlinks-description": "Trouver toutes les pages qui ont un lien vers la page donnée.",
        "apihelp-query+backlinks-param-title": "Titre à rechercher. Impossible à utiliser avec <var>$1pageid</var>.",
        "apihelp-query+userinfo-paramvalue-prop-acceptlang": "Renvoie en écho l’entête <code>Accept-Language</code> envoyé par le client dans un format structuré.",
        "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Ajoute la date d’inscription de l’utilisateur.",
        "apihelp-query+userinfo-paramvalue-prop-unreadcount": "Ajoute le compteur de pages non lues de la liste de suivi de l’utilisateur (au maximum $1 ; renvoie <samp>$2</samp> s’il y en a plus).",
+       "apihelp-query+userinfo-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.",
+       "apihelp-query+userinfo-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.",
        "apihelp-query+userinfo-example-simple": "Obtenir de l’information sur l’utilisateur actuel",
        "apihelp-query+userinfo-example-data": "Obtenir des informations supplémentaires sur l’utilisateur actuel",
        "apihelp-query+users-description": "Obtenir des information sur une liste d’utilisateurs",
        "apihelp-query+users-paramvalue-prop-registration": "Ajoute l’horodatage d’inscription de l’utilisateur.",
        "apihelp-query+users-paramvalue-prop-emailable": "Marque si l’utilisateur peut et veut recevoir des courriels via [[Special:Emailuser]].",
        "apihelp-query+users-paramvalue-prop-gender": "Marque le sexe de l’utilisateur. Renvoie « male », « female », ou « unknown ».",
+       "apihelp-query+users-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.",
+       "apihelp-query+users-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.",
        "apihelp-query+users-param-users": "Une liste des utilisateurs sur lesquels obtenir de l’information.",
        "apihelp-query+users-param-token": "Utiliser plutôt <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
        "apihelp-query+users-example-simple": "Renvoyer des informations pour l'utilisateur <kbd>Example</kbd>.",
index 60e9f6d..b2fe5b8 100644 (file)
        "apihelp-query+allusers-paramvalue-prop-rights": "רשימת הההרשאות שיש למשתמש.",
        "apihelp-query+allusers-paramvalue-prop-editcount": "הוספת מניין העריכות של המשתמש .",
        "apihelp-query+allusers-paramvalue-prop-registration": "הוספת חותם־הזמן של זמן הרישום של המשתמש (יכול להיות ריק).",
+       "apihelp-query+allusers-paramvalue-prop-centralids": "הוספת המזהה המרכזי ומצב השיוך למשתמש.",
        "apihelp-query+allusers-param-limit": "כמה שמות משתמש בסך הכול לשנות.",
        "apihelp-query+allusers-param-witheditsonly": "לרשום רק משתמשים שעשו עריכות.",
        "apihelp-query+allusers-param-activeusers": "לרשום רק משתמשים שהיו פעילים {{PLURAL:$1|ביום האחרון|ביומיים האחרונים|ב־$1 הימים האחרונים}}.",
+       "apihelp-query+allusers-param-attachedwiki": "עם <kbd>$1prop=centralids</kbd>, לציין גם האם המשתמש משויך לוויקי עם המזהה הזה.",
        "apihelp-query+allusers-example-Y": "לרשום משתמשים שמתחילים ב־<kbd>Y</kbd>.",
        "apihelp-query+backlinks-description": "מציאת כל הדפים שמקשרים לדף הנתון.",
        "apihelp-query+backlinks-param-title": "איזו כותרת לחפש. לא ניתן להשתמש בזה יחד עם <var>$1pageid</var>.",
        "apihelp-query+userinfo-paramvalue-prop-acceptlang": "מדפיס את כותרת <code>Accept-Language</code> ששלח הלקוח בתסדיר מובנה.",
        "apihelp-query+userinfo-paramvalue-prop-registrationdate": "הוספת תאריך הרישום של המשתמש.",
        "apihelp-query+userinfo-paramvalue-prop-unreadcount": "הוספת מניין הדפים שלא נקראו ברשימת המעקב של המשתמש (לכל היותר $1; מחזיר <samp>$2</samp> אם יש יותר).",
+       "apihelp-query+userinfo-paramvalue-prop-centralids": "הוספת המזהה המרכזי ומצב השיוך למשתמש.",
+       "apihelp-query+userinfo-param-attachedwiki": "עם <kbd>$1prop=centralids</kbd>, לציין האם המשתמש משויך לוויקי עם המזהה הזה.",
        "apihelp-query+userinfo-example-simple": "קבלת מידע על המשתמש הנוכחי.",
        "apihelp-query+userinfo-example-data": "קבלת מידע נוסף על המשתמש הנוכחי.",
        "apihelp-query+users-description": "קבלת מידע על רשימת משתמשים.",
        "apihelp-query+users-paramvalue-prop-registration": "הוספת חותם־הזמן של רישום המשתמש.",
        "apihelp-query+users-paramvalue-prop-emailable": "מתייג אם המשתמש יכול ורוצה לקבל דואר אלקטרוני דרך [[Special:Emailuser]].",
        "apihelp-query+users-paramvalue-prop-gender": "מתייג את המגדר של המשתמש. מחזיר \"male\"‏, \"female\" או \"unknown\".",
+       "apihelp-query+users-paramvalue-prop-centralids": "הוספת המזהה המרכזי ומצב השיוך למשתמש.",
+       "apihelp-query+users-param-attachedwiki": "עם <kbd>$1prop=centralids</kbd>, לציין האם המשתמש משויך לוויקי עם המזהה הזה.",
        "apihelp-query+users-param-users": "רשימת משתמשים שעליהם צריך לקבל מידע.",
        "apihelp-query+users-param-token": "יש להשתמש ב־<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> במקום.",
        "apihelp-query+users-example-simple": "החזרת מידע עבור המשתמש <kbd>Example</kbd>.",
index 1a0e181..432d9c0 100644 (file)
        "apihelp-query+allusers-paramvalue-prop-rights": "सदस्यास असलेल्या अधिकारांची यादी करते.",
        "apihelp-query+allusers-paramvalue-prop-editcount": "सदस्याची संपादन मोजणी जोडते.",
        "apihelp-query+allusers-paramvalue-prop-registration": "जर उपलब्ध असेल तर,सदस्याने केंव्हा नोंदणी केली त्याचा वेळठसा(रिक्त असू शकतो)",
+       "apihelp-query+allusers-paramvalue-prop-centralids": "सदस्याची केंद्रीय ओळखण्या व जुळल्याची स्थिती जोडते.",
        "apihelp-query+allusers-param-witheditsonly": "फक्त संपादन केलेल्या सदस्यांचीच यादी करा.",
        "apihelp-query+allusers-param-activeusers": "मागील $1 {{PLURAL:$1|दिवसात}} सक्रिय सदस्यांचीच यादी करा.",
+       "apihelp-query+allusers-param-attachedwiki": "<kbd>$1prop=centralids</kbd> याद्वारे असेही दर्शविण्यात येते कि सदस्य हा या विकिशी जुळलेला असून तो या ओळखणीद्वारे ओळखल्या जातो.",
        "apihelp-query+allusers-example-Y": "<kbd>य</kbd> पासून सदस्यनाव सुरु होणाऱ्या सदस्यांचीच यादी करा.",
        "apihelp-query+backlinks-description": "दिलेल्या पानास दुवे असणारी सर्व पाने शोधा.",
        "apihelp-query+backlinks-param-title": "शोधावयाचे शीर्षक.<var>$1pageid</var>यासमवेत वापरु शकत नाही.",
        "apihelp-query+langlinks-param-dir": "कोणत्या दिशेस यादी करावयाची.",
        "apihelp-query+links-param-dir": "कोणत्या दिशेस यादी करावयाची.",
        "apihelp-query+recentchanges-param-end": "कुठपर्यंत प्रगणना संपवायची त्याचा वेळठसा.",
+       "apihelp-query+userinfo-paramvalue-prop-centralids": "सदस्याची केंद्रीय ओळखण्या व जुळल्याची स्थिती जोडते.",
+       "apihelp-query+userinfo-param-attachedwiki": "<kbd>$1prop=centralids</kbd> याद्वारे असे दर्शविण्यात येते कि सदस्य हा या विकिशी जुळलेला असून तो या ओळखणीद्वारे ओळखल्या जातो.",
+       "apihelp-query+users-paramvalue-prop-centralids": "सदस्याची केंद्रीय ओळखण्या व जुळल्याची स्थिती जोडते.",
+       "apihelp-query+users-param-attachedwiki": "<kbd>$1prop=centralids</kbd> याद्वारे असे दर्शविण्यात येते कि सदस्य हा या विकिशी जुळलेला असून तो या ओळखणीद्वारे ओळखल्या जातो.",
        "apihelp-query+watchlist-param-type": "कोणत्या प्रकारचे बदल दाखवायचे:",
        "apihelp-query+watchlist-paramvalue-type-edit": "नित्याची पान संपादने.",
        "apihelp-query+watchlist-paramvalue-type-external": "बाह्य बदल.",
index 047a806..0cd7596 100644 (file)
        "apihelp-query+allusers-paramvalue-prop-rights": "{{doc-apihelp-paramvalue|query+allusers|prop|rights}}",
        "apihelp-query+allusers-paramvalue-prop-editcount": "{{doc-apihelp-paramvalue|query+allusers|prop|editcount}}",
        "apihelp-query+allusers-paramvalue-prop-registration": "{{doc-apihelp-paramvalue|query+allusers|prop|registration}}",
+       "apihelp-query+allusers-paramvalue-prop-centralids": "{{doc-apihelp-paramvalue|query+allusers|prop|centralids}}",
        "apihelp-query+allusers-param-limit": "{{doc-apihelp-param|query+allusers|limit}}",
        "apihelp-query+allusers-param-witheditsonly": "{{doc-apihelp-param|query+allusers|witheditsonly}}",
        "apihelp-query+allusers-param-activeusers": "{{doc-apihelp-param|query+allusers|activeusers|params=* $1 - Value of [[mw:Manual:$wgActiveUserDays]]|paramstart=2}}",
+       "apihelp-query+allusers-param-attachedwiki": "{{doc-apihelp-param|query+allusers|attachedwiki}}",
        "apihelp-query+allusers-example-Y": "{{doc-apihelp-example|query+allusers}}",
        "apihelp-query+backlinks-description": "{{doc-apihelp-description|query+backlinks}}",
        "apihelp-query+backlinks-param-title": "{{doc-apihelp-param|query+backlinks|title}}",
        "apihelp-query+userinfo-paramvalue-prop-acceptlang": "{{doc-apihelp-paramvalue|query+userinfo|prop|acceptlang}}",
        "apihelp-query+userinfo-paramvalue-prop-registrationdate": "{{doc-apihelp-paramvalue|query+userinfo|prop|registrationdate}}",
        "apihelp-query+userinfo-paramvalue-prop-unreadcount": "{{doc-apihelp-paramvalue|query+userinfo|prop|unreadcount|params=* $1 - Maximum value for the \"unreadcount\" property.\n* $2 - Return value when there are more unread pages.|paramstart=3}}",
+       "apihelp-query+userinfo-paramvalue-prop-centralids": "{{doc-apihelp-paramvalue|query+userinfo|prop|centralids}}",
+       "apihelp-query+userinfo-param-attachedwiki": "{{doc-apihelp-param|query+userinfo|attachedwiki}}",
        "apihelp-query+userinfo-example-simple": "{{doc-apihelp-example|query+userinfo}}",
        "apihelp-query+userinfo-example-data": "{{doc-apihelp-example|query+userinfo}}",
        "apihelp-query+users-description": "{{doc-apihelp-description|query+users}}",
        "apihelp-query+users-paramvalue-prop-registration": "{{doc-apihelp-paramvalue|query+users|prop|registration}}",
        "apihelp-query+users-paramvalue-prop-emailable": "{{doc-apihelp-paramvalue|query+users|prop|emailable}}",
        "apihelp-query+users-paramvalue-prop-gender": "{{doc-apihelp-paramvalue|query+users|prop|gender}}",
+       "apihelp-query+users-paramvalue-prop-centralids": "{{doc-apihelp-paramvalue|query+users|prop|centralids}}",
+       "apihelp-query+users-param-attachedwiki": "{{doc-apihelp-param|query+users|attachedwiki}}",
        "apihelp-query+users-param-users": "{{doc-apihelp-param|query+users|users}}",
        "apihelp-query+users-param-token": "{{doc-apihelp-param|query+users|token}}",
        "apihelp-query+users-example-simple": "{{doc-apihelp-example|query+users}}",
index b4086f9..af6f9d9 100644 (file)
@@ -178,6 +178,7 @@ class CategoryMembershipChange {
                        }
                }
 
+               /** @var RecentChange $rc */
                $rc = call_user_func_array(
                        $this->newForCategorizationCallback,
                        array(
index c0cf067..68f94b6 100644 (file)
@@ -3820,7 +3820,7 @@ abstract class DatabaseBase implements IDatabase {
         * @param IDatabase $db1
         * @param IDatabase ...
         * @return array Map of values:
-        *   - lag: highest lag of any of the DBs
+        *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
         *   - since: oldest UNIX timestamp of any of the DB lag estimates
         *   - pending: whether any of the DBs have uncommitted changes
         * @since 1.27
@@ -3830,7 +3830,11 @@ abstract class DatabaseBase implements IDatabase {
                foreach ( func_get_args() as $db ) {
                        /** @var IDatabase $db */
                        $status = $db->getSessionLagStatus();
-                       $res['lag'] = max( $res['lag'], $status['lag'] );
+                       if ( $status['lag'] === false ) {
+                               $res['lag'] = false;
+                       } elseif ( $res['lag'] !== false ) {
+                               $res['lag'] = max( $res['lag'], $status['lag'] );
+                       }
                        $res['since'] = min( $res['since'], $status['since'] );
                        $res['pending'] = $res['pending'] ?: $db->writesPending();
                }
index 09954a0..510d42a 100644 (file)
@@ -37,6 +37,10 @@ use Monolog\Formatter\FormatterInterface;
  * @copyright © 2015 Erik Bernhardson and Wikimedia Foundation.
  */
 class AvroFormatter implements FormatterInterface {
+       /**
+        * @var Magic byte to encode schema revision id.
+        */
+       const MAGIC = 0x0;
        /**
         * @var array Map from schema name to schema definition
         */
@@ -80,6 +84,7 @@ class AvroFormatter implements FormatterInterface {
        public function format( array $record ) {
                $this->io->truncate();
                $schema = $this->getSchema( $record['channel'] );
+               $revId = $this->getSchemaRevisionId( $record['channel'] );
                if ( $schema === null ) {
                        trigger_error( "The schema for channel '{$record['channel']}' is not available" );
                        return null;
@@ -92,6 +97,10 @@ class AvroFormatter implements FormatterInterface {
                        trigger_error( "Avro failed to serialize record for {$record['channel']} : {$json}" );
                        return null;
                }
+               if ( $revId !== null ) {
+                       return chr( self::MAGIC ) . $this->encode_long( $revId ) . $this->io->string();
+               }
+               // @todo: remove backward compat code and do not send messages without rev id.
                return $this->io->string();
        }
 
@@ -123,17 +132,55 @@ class AvroFormatter implements FormatterInterface {
                if ( !isset( $this->schemas[$channel] ) ) {
                        return null;
                }
-               if ( !$this->schemas[$channel] instanceof AvroSchema ) {
-                       if ( is_string( $this->schemas[$channel] ) ) {
-                               $this->schemas[$channel] = AvroSchema::parse( $this->schemas[$channel] );
+               $schemaDetails = &$this->schemas[$channel];
+               $schema = null;
+               if ( isset( $schemaDetails['revision'] ) && isset( $schemaDetails['schema'] ) ) {
+                       $schema = &$schemaDetails['schema'];
+               } else {
+                       // @todo: Remove backward compat code
+                       $schema = &$schemaDetails;
+               }
+
+               if ( !$schema instanceof AvroSchema ) {
+                       if ( is_string( $schema ) ) {
+                               $schema = AvroSchema::parse( $schema );
                        } else {
-                               $this->schemas[$channel] = AvroSchema::real_parse(
+                               $schema = AvroSchema::real_parse(
                                        $this->schemas[$channel],
                                        null,
                                        new AvroNamedSchemata()
                                );
                        }
                }
-               return $this->schemas[$channel];
+               return $schema;
+       }
+
+       /**
+        * Get the writer for the named channel
+        *
+        * @var string $channel Name of the schema
+        * @return int|null
+        */
+       public function getSchemaRevisionId( $channel ) {
+               // @todo: remove backward compat code
+               if ( isset( $this->schemas[$channel] )
+                               && is_array( $this->schemas[$channel] )
+                               && isset( $this->schemas[$channel]['revision'] ) ) {
+                       return (int) $this->schemas[$channel]['revision'];
+               }
+               return null;
+       }
+
+
+       /**
+        * convert an integer to a 64bits big endian long (Java compatible)
+        * NOTE: certainly only compatible with PHP 64bits
+        * @param int $id
+        * @return string the binary representation of $id
+        */
+       private function encode_long( $id ) {
+               $high   = ( $id & 0xffffffff00000000 ) >> 32;
+               $low    = $id & 0x00000000ffffffff;
+               return pack( 'NN', $high, $low );
        }
 }
diff --git a/includes/deferred/DeferrableUpdate.php b/includes/deferred/DeferrableUpdate.php
new file mode 100644 (file)
index 0000000..5f4d821
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * Interface that deferrable updates should implement. Basically required so we
+ * can validate input on DeferredUpdates::addUpdate()
+ *
+ * @since 1.19
+ */
+interface DeferrableUpdate {
+       /**
+        * Perform the actual work
+        */
+       function doUpdate();
+}
index 8eec202..adad908 100644 (file)
  * @file
  */
 
-/**
- * Interface that deferrable updates should implement. Basically required so we
- * can validate input on DeferredUpdates::addUpdate()
- *
- * @since 1.19
- */
-interface DeferrableUpdate {
-       /**
-        * Perform the actual work
-        */
-       function doUpdate();
-}
-
 /**
  * Class for managing the deferred updates
  *
- * Deferred updates can be run at the end of the request,
- * after the HTTP response has been sent. In CLI mode, updates
- * are only deferred until there is no local master DB transaction.
- * When updates are deferred, they go into a simple FIFO queue.
+ * In web request mode, deferred updates can be run at the end of the request, either before or
+ * after the HTTP response has been sent. In either case, they run after the DB commit step. If
+ * an update runs after the response is sent, it will not block clients. If sent before, it will
+ * run synchronously. If such an update works via queueing, it will be more likely to complete by
+ * the time the client makes their next request after this one.
+ *
+ * In CLI mode, updates are only deferred until the current wiki has no DB write transaction
+ * active within this request.
+ *
+ * When updates are deferred, they use a FIFO queue (one for pre-send and one for post-send).
  *
  * @since 1.19
  */
 class DeferredUpdates {
-       /** @var DeferrableUpdate[] Updates to be deferred until the end of the request */
-       private static $updates = array();
+       /** @var DeferrableUpdate[] Updates to be deferred until before request end */
+       private static $preSendUpdates = array();
+       /** @var DeferrableUpdate[] Updates to be deferred until after request end */
+       private static $postSendUpdates = array();
+
        /** @var bool Defer updates fully even in CLI mode */
        private static $forceDeferral = false;
 
+       const ALL = 0; // all updates
+       const PRESEND = 1; // for updates that should run before flushing output buffer
+       const POSTSEND = 2; // for updates that should run after flushing output buffer
+
        /**
         * Add an update to the deferred list
+        *
         * @param DeferrableUpdate $update Some object that implements doUpdate()
+        * @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
         */
-       public static function addUpdate( DeferrableUpdate $update ) {
+       public static function addUpdate( DeferrableUpdate $update, $type = self::POSTSEND ) {
+               if ( $type === self::PRESEND ) {
+                       self::push( self::$preSendUpdates, $update );
+               } else {
+                       self::push( self::$postSendUpdates, $update );
+               }
+       }
+
+       /**
+        * Add a callable update.  In a lot of cases, we just need a callback/closure,
+        * defining a new DeferrableUpdate object is not necessary
+        *
+        * @see MWCallableUpdate::__construct()
+        *
+        * @param callable $callable
+        * @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
+        */
+       public static function addCallableUpdate( $callable, $type = self::POSTSEND ) {
+               self::addUpdate( new MWCallableUpdate( $callable ), $type );
+       }
+
+       /**
+        * Do any deferred updates and clear the list
+        *
+        * @param string $mode Use "enqueue" to use the job queue when possible [Default: "run"]
+        * @param integer $type DeferredUpdates constant (PRESEND, POSTSEND, or ALL) (since 1.27)
+        */
+       public static function doUpdates( $mode = 'run', $type = self::ALL ) {
+               if ( $type === self::ALL || $type == self::PRESEND ) {
+                       self::execute( self::$preSendUpdates, $mode );
+               }
+
+               if ( $type === self::ALL || $type == self::POSTSEND ) {
+                       self::execute( self::$postSendUpdates, $mode );
+               }
+       }
+
+       private static function push( array &$queue, DeferrableUpdate $update ) {
                global $wgCommandLineMode;
 
-               array_push( self::$updates, $update );
+               if ( $update instanceof MergeableUpdate ) {
+                       $class = get_class( $update ); // fully-qualified class
+                       if ( isset( $queue[$class] ) ) {
+                               /** @var $existingUpdate MergeableUpdate */
+                               $existingUpdate = $queue[$class];
+                               $existingUpdate->merge( $update );
+                       } else {
+                               $queue[$class] = $update;
+                       }
+               } else {
+                       $queue[] = $update;
+               }
+
                if ( self::$forceDeferral ) {
-                       return;
+                       return; // do not run
                }
 
                // CLI scripts may forget to periodically flush these updates,
-               // so try to handle that rather than OOMing and losing them.
-               // Try to run the updates as soon as there is no local transaction.
+               // so try to handle that rather than OOMing and losing them entirely.
+               // Try to run the updates as soon as there is no current wiki transaction.
                static $waitingOnTrx = false; // de-duplicate callback
                if ( $wgCommandLineMode && !$waitingOnTrx ) {
                        $lb = wfGetLB();
@@ -81,34 +132,12 @@ class DeferredUpdates {
                }
        }
 
-       /**
-        * Add a callable update.  In a lot of cases, we just need a callback/closure,
-        * defining a new DeferrableUpdate object is not necessary
-        * @see MWCallableUpdate::__construct()
-        * @param callable $callable
-        */
-       public static function addCallableUpdate( $callable ) {
-               self::addUpdate( new MWCallableUpdate( $callable ) );
-       }
-
-       /**
-        * Do any deferred updates and clear the list
-        *
-        * @param string $mode Use "enqueue" to use the job queue when possible [Default: run]
-        *   prevent lock contention
-        * @param string $oldMode Unused
-        */
-       public static function doUpdates( $mode = 'run', $oldMode = '' ) {
-               // B/C for ( $commit, $mode ) args
-               $mode = $oldMode ?: $mode;
-               if ( $mode === 'commit' ) {
-                       $mode = 'run';
-               }
-
-               $updates = self::$updates;
+       public static function execute( array &$queue, $mode ) {
+               $updates = $queue; // snapshot of queue
 
+               // Keep doing rounds of updates until none get enqueued
                while ( count( $updates ) ) {
-                       self::clearPendingUpdates();
+                       $queue = array(); // clear the queue
                        /** @var DataUpdate[] $dataUpdates */
                        $dataUpdates = array();
                        /** @var DeferrableUpdate[] $otherUpdates */
@@ -140,7 +169,7 @@ class DeferredUpdates {
                                }
                        }
 
-                       $updates = self::$updates;
+                       $updates = $queue; // new snapshot of queue (check for new entries)
                }
        }
 
@@ -149,7 +178,8 @@ class DeferredUpdates {
         * want or need to call this. Unit tests need it though.
         */
        public static function clearPendingUpdates() {
-               self::$updates = array();
+               self::$preSendUpdates = array();
+               self::$postSendUpdates = array();
        }
 
        /**
index a480aec..db3790f 100644 (file)
@@ -43,24 +43,8 @@ class HTMLCacheUpdate implements DeferrableUpdate {
        }
 
        public function doUpdate() {
-               $job = new HTMLCacheUpdateJob(
-                       $this->mTitle,
-                       array(
-                               'table' => $this->mTable,
-                               'recursive' => true
-                       ) + Job::newRootJobParams( // "overall" refresh links job info
-                               "htmlCacheUpdate:{$this->mTable}:{$this->mTitle->getPrefixedText()}"
-                       )
-               );
+               $job = HTMLCacheUpdateJob::newForBacklinks( $this->mTitle, $this->mTable );
 
-               $count = $this->mTitle->getBacklinkCache()->getNumLinks( $this->mTable, 100 );
-               if ( $count >= 100 ) { // many backlinks
-                       JobQueueGroup::singleton()->lazyPush( $job );
-               } else { // few backlinks ($count might be off even if 0)
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->onTransactionIdle( function () use ( $job ) {
-                               $job->run(); // just do the purge query now
-                       } );
-               }
+               JobQueueGroup::singleton()->lazyPush( $job );
        }
 }
index c253e74..f9d7e9c 100644 (file)
@@ -153,8 +153,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
        }
 
        protected function doIncrementalUpdate() {
-               global $wgRCWatchCategoryMembership;
-
                # Page links
                $existing = $this->getExistingLinks();
                $this->linkDeletions = $this->getLinkDeletions( $existing );
@@ -206,14 +204,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
                $this->invalidateCategories( $categoryUpdates );
                $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
 
-               # Category membership changes
-               if (
-                       $wgRCWatchCategoryMembership &&
-                       !$this->mTriggeredRecursive && ( $categoryInserts || $categoryDeletes )
-               ) {
-                       $this->triggerCategoryChanges( $categoryInserts, $categoryDeletes );
-               }
-
                # Page properties
                $existing = $this->getExistingProperties();
 
@@ -237,24 +227,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
        }
 
-       private function triggerCategoryChanges( $categoryInserts, $categoryDeletes ) {
-               $catMembChange = new CategoryMembershipChange( $this->mTitle, $this->mRevision );
-
-               if ( $this->mRecursive ) {
-                       $catMembChange->checkTemplateLinks();
-               }
-
-               foreach ( $categoryInserts as $categoryName => $value ) {
-                       $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY );
-                       $catMembChange->triggerCategoryAddedNotification( $categoryTitle );
-               }
-
-               foreach ( $categoryDeletes as $categoryName => $value ) {
-                       $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY );
-                       $catMembChange->triggerCategoryRemovedNotification( $categoryTitle );
-               }
-       }
-
        /**
         * Queue recursive jobs for this page
         *
diff --git a/includes/deferred/MergeableUpdate.php b/includes/deferred/MergeableUpdate.php
new file mode 100644 (file)
index 0000000..70760ce
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * Interface that deferrable updates can implement. DeferredUpdates uses this to merge
+ * all pending updates of PHP class into a single update by calling merge().
+ *
+ * @since 1.27
+ */
+interface MergeableUpdate {
+       /**
+        * Merge this update with $update
+        *
+        * @param MergeableUpdate $update Update of the same class type
+        */
+       function merge( MergeableUpdate $update );
+}
index 6ed1d00..2abf028 100644 (file)
@@ -38,6 +38,9 @@ class SearchUpdate implements DeferrableUpdate {
        /** @var Content|bool Content of the page (not text) */
        private $content;
 
+       /** @var WikiPage **/
+       private $page;
+
        /**
         * Constructor
         *
@@ -78,18 +81,15 @@ class SearchUpdate implements DeferrableUpdate {
                        return;
                }
 
-               $page = WikiPage::newFromID( $this->id, WikiPage::READ_LATEST );
-
                foreach ( SearchEngine::getSearchTypes() as $type ) {
                        $search = SearchEngine::create( $type );
-                       $indexTitle = $this->indexTitle( $search );
                        if ( !$search->supports( 'search-update' ) ) {
                                continue;
                        }
 
-                       $normalTitle = $search->normalizeText( $indexTitle );
+                       $normalTitle = $this->getNormalizedTitle( $search );
 
-                       if ( $page === null ) {
+                       if ( $this->getLatestPage() === null ) {
                                $search->delete( $this->id, $normalTitle );
                                continue;
                        } elseif ( $this->content === false ) {
@@ -174,13 +174,30 @@ class SearchUpdate implements DeferrableUpdate {
        }
 
        /**
-        * Get a string representation of a title suitable for
+        * Get WikiPage for the SearchUpdate $id using WikiPage::READ_LATEST
+        * and ensure using the same WikiPage object if there are multiple
+        * SearchEngine types.
+        *
+        * Returns null if a page has been deleted or is not found.
+        *
+        * @return WikiPage|null
+        */
+       private function getLatestPage() {
+               if ( !isset( $this->page ) ) {
+                       $this->page = WikiPage::newFromID( $this->id, WikiPage::READ_LATEST );
+               }
+
+               return $this->page;
+       }
+
+       /**
+        * Get a normalized string representation of a title suitable for
         * including in a search index
         *
         * @param SearchEngine $search
         * @return string A stripped-down title string ready for the search index
         */
-       private function indexTitle( SearchEngine $search ) {
+       private function getNormalizedTitle( SearchEngine $search ) {
                global $wgContLang;
 
                $ns = $this->title->getNamespace();
@@ -200,6 +217,7 @@ class SearchUpdate implements DeferrableUpdate {
                if ( $ns == NS_FILE ) {
                        $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
                }
-               return trim( $t );
+
+               return $search->normalizeText( trim( $t ) );
        }
 }
index d33e4a5..c223de8 100644 (file)
  * @ingroup Cache
  */
 
+use Wikimedia\Assert\Assert;
+
 /**
  * Handles purging appropriate Squid URLs given a title (or titles)
  * @ingroup Cache
  */
-class SquidUpdate implements DeferrableUpdate {
+class SquidUpdate implements DeferrableUpdate, MergeableUpdate {
        /** @var string[] Collection of URLs to purge */
        protected $urls = array();
 
@@ -33,8 +35,7 @@ class SquidUpdate implements DeferrableUpdate {
         * @param string[] $urlArr Collection of URLs to purge
         */
        public function __construct( array $urlArr ) {
-               // Remove duplicate URLs from list
-               $this->urls = array_unique( $urlArr );
+               $this->urls = $urlArr;
        }
 
        /**
@@ -59,9 +60,7 @@ class SquidUpdate implements DeferrableUpdate {
         * @deprecated 1.27
         */
        public static function newSimplePurge( Title $title ) {
-               $urlArr = $title->getSquidURLs();
-
-               return new SquidUpdate( $urlArr );
+               return new SquidUpdate( $title->getSquidURLs() );
        }
 
        /**
@@ -71,6 +70,13 @@ class SquidUpdate implements DeferrableUpdate {
                self::purge( $this->urls );
        }
 
+       public function merge( MergeableUpdate $update ) {
+               /** @var SquidUpdate $update */
+               Assert::parameterType( __CLASS__, $update, '$update' );
+
+               $this->urls = array_merge( $this->urls, $update->urls );
+       }
+
        /**
         * Purges a list of Squids defined in $wgSquidServers.
         * $urlArr should contain the full URLs to purge as values
@@ -86,6 +92,9 @@ class SquidUpdate implements DeferrableUpdate {
                        return;
                }
 
+               // Remove duplicate URLs from list
+               $urlArr = array_unique( $urlArr );
+
                wfDebugLog( 'squid', __METHOD__ . ': ' . implode( ' ', $urlArr ) );
 
                if ( $wgHTCPRouting ) {
@@ -93,8 +102,6 @@ class SquidUpdate implements DeferrableUpdate {
                }
 
                if ( $wgSquidServers ) {
-                       // Remove duplicate URLs
-                       $urlArr = array_unique( $urlArr );
                        // Maximum number of parallel connections per squid
                        $maxSocketsPerSquid = 8;
                        // Number of requests to send per socket
@@ -127,7 +134,7 @@ class SquidUpdate implements DeferrableUpdate {
         * @throws MWException
         * @param string[] $urlArr Collection of URLs to purge
         */
-       protected static function HTCPPurge( array $urlArr ) {
+       private static function HTCPPurge( array $urlArr ) {
                global $wgHTCPRouting, $wgHTCPMulticastTTL;
 
                // HTCP CLR operation
@@ -158,8 +165,6 @@ class SquidUpdate implements DeferrableUpdate {
                                $wgHTCPMulticastTTL );
                }
 
-               // Remove duplicate URLs from collection
-               $urlArr = array_unique( $urlArr );
                // Get sequential trx IDs for packet loss counting
                $ids = UIDGenerator::newSequentialPerNodeIDs(
                        'squidhtcppurge', 32, count( $urlArr ), UIDGenerator::QUICK_VOLATILE
index c2bbb4e..dcc8741 100644 (file)
@@ -879,7 +879,10 @@ class LocalFile extends File {
                $this->purgeThumbnails( $options );
 
                // Purge squid cache for this file
-               SquidUpdate::purge( array( $this->getURL() ) );
+               DeferredUpdates::addUpdate(
+                       new SquidUpdate( array( $this->getUrl() ) ),
+                       DeferredUpdates::PRESEND
+               );
        }
 
        /**
@@ -887,8 +890,6 @@ class LocalFile extends File {
         * @param string $archiveName Name of the archived file
         */
        function purgeOldThumbnails( $archiveName ) {
-               global $wgUseSquid;
-
                // Get a list of old thumbnails and URLs
                $files = $this->getThumbnails( $archiveName );
 
@@ -899,14 +900,11 @@ class LocalFile extends File {
                $this->purgeThumbList( $dir, $files );
 
                // Purge the squid
-               if ( $wgUseSquid ) {
-                       $urls = array();
-                       foreach ( $files as $file ) {
-                               $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
-                       }
-                       SquidUpdate::purge( $urls );
+               $urls = array();
+               foreach ( $files as $file ) {
+                       $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
                }
-
+               DeferredUpdates::addUpdate( new SquidUpdate( $urls ), DeferredUpdates::PRESEND );
        }
 
        /**
@@ -914,18 +912,14 @@ class LocalFile extends File {
         * @param array $options
         */
        public function purgeThumbnails( $options = array() ) {
-               global $wgUseSquid;
-
                // Delete thumbnails
                $files = $this->getThumbnails();
                // Always purge all files from squid regardless of handler filters
                $urls = array();
-               if ( $wgUseSquid ) {
-                       foreach ( $files as $file ) {
-                               $urls[] = $this->getThumbUrl( $file );
-                       }
-                       array_shift( $urls ); // don't purge directory
+               foreach ( $files as $file ) {
+                       $urls[] = $this->getThumbUrl( $file );
                }
+               array_shift( $urls ); // don't purge directory
 
                // Give media handler a chance to filter the file purge list
                if ( !empty( $options['forThumbRefresh'] ) ) {
@@ -942,10 +936,7 @@ class LocalFile extends File {
                $this->purgeThumbList( $dir, $files );
 
                // Purge the squid
-               if ( $wgUseSquid ) {
-                       SquidUpdate::purge( $urls );
-               }
-
+               DeferredUpdates::addUpdate( new SquidUpdate( $urls ), DeferredUpdates::PRESEND );
        }
 
        /**
@@ -1444,7 +1435,10 @@ class LocalFile extends File {
                                # Delete old thumbnails
                                $that->purgeThumbnails();
                                # Remove the old file from the squid cache
-                               SquidUpdate::purge( array( $that->getURL() ) );
+                               DeferredUpdates::addUpdate(
+                                       new SquidUpdate( array( $that->getUrl() ) ),
+                                       DeferredUpdates::PRESEND
+                               );
                        } else {
                                # Update backlink pages pointing to this title if created
                                LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
@@ -1631,24 +1625,20 @@ class LocalFile extends File {
                $that = $this;
                $this->getRepo()->getMasterDB()->onTransactionIdle(
                        function () use ( $that, $archiveNames ) {
-                               global $wgUseSquid;
-
                                $that->purgeEverything();
                                foreach ( $archiveNames as $archiveName ) {
                                        $that->purgeOldThumbnails( $archiveName );
                                }
-
-                               if ( $wgUseSquid ) {
-                                       // Purge the squid
-                                       $purgeUrls = array();
-                                       foreach ( $archiveNames as $archiveName ) {
-                                               $purgeUrls[] = $that->getArchiveUrl( $archiveName );
-                                       }
-                                       SquidUpdate::purge( $purgeUrls );
-                               }
                        }
                );
 
+               // Purge the squid
+               $purgeUrls = array();
+               foreach ( $archiveNames as $archiveName ) {
+                       $purgeUrls[] = $this->getArchiveUrl( $archiveName );
+               }
+               DeferredUpdates::addUpdate( new SquidUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
+
                return $status;
        }
 
@@ -1668,7 +1658,6 @@ class LocalFile extends File {
         * @return FileRepoStatus
         */
        function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
-               global $wgUseSquid;
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
                        return $this->readOnlyFatalStatus();
                }
@@ -1685,10 +1674,10 @@ class LocalFile extends File {
                        $this->purgeDescription();
                }
 
-               if ( $wgUseSquid ) {
-                       // Purge the squid
-                       SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
-               }
+               DeferredUpdates::addUpdate(
+                       new SquidUpdate( array( $this->getArchiveUrl( $archiveName ) ) ),
+                       DeferredUpdates::PRESEND
+               );
 
                return $status;
        }
index 1a77d18..44c6b14 100644 (file)
@@ -80,7 +80,7 @@
        "config-charset-mysql5-binary": "MySQL 4.1/5.0 δυαδικό",
        "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
        "config-charset-mysql4": "UTF-8 συμβατό προς τα πίσω με MySQL 4.0",
-       "config-mysql-old": "Î\91Ï\80αιÏ\84είÏ\84αι Microsoft SQL Server $1 Î® Î½ÎµÏ\8eτερο. Εσείς έχετε $2.",
+       "config-mysql-old": "Î\91Ï\80αιÏ\84είÏ\84αι Microsoft SQL Server $1 Î® Î½ÎµÏ\8cτερο. Εσείς έχετε $2.",
        "config-db-port": "Θύρα βάσης δεδομένων:",
        "config-db-schema": "Σχήμα για MediaWiki:",
        "config-db-schema-help": "Αυτό το σχήμα συνήθως θα είναι εντάξει.\nΆλλαξε το μόνο αν ξέρεις ότι το χρειάζεσαι.",
        "config-missing-db-server-oracle": "Πρέπει να εισαγάγετε μια τιμή για \"{{int:config-db-host-oracle}}\".",
        "config-connection-error": "$1.\n\nΕλέγξτε τη διεύθυνση, το όνομα χρήστη και τον κωδικό πρόσβασης και προσπαθήστε ξανά.",
        "config-db-sys-user-exists-oracle": "Ο λογαριασμός χρήστη \"$1\" υπάρχει ήδη. Το SYSDBA μπορεί να χρησιμοποιηθεί μόνο για τη δημιουργία ενός νέου λογαριασμού!",
-       "config-postgres-old": "Î\91Ï\80αιÏ\84είÏ\84αι PostgreSQL $1 Î® Î½ÎµÏ\8eτερο. Εσείς έχετε $2.",
-       "config-mssql-old": "Î\91Ï\80αιÏ\84είÏ\84αι Microsoft SQL Server $1 Î® Î½ÎµÏ\8eτερο. Εσείς έχετε $2.",
+       "config-postgres-old": "Î\91Ï\80αιÏ\84είÏ\84αι PostgreSQL $1 Î® Î½ÎµÏ\8cτερο. Εσείς έχετε $2.",
+       "config-mssql-old": "Î\91Ï\80αιÏ\84είÏ\84αι Microsoft SQL Server $1 Î® Î½ÎµÏ\8cτερο. Εσείς έχετε $2.",
        "config-sqlite-readonly": "Το αρχείο <code>$1</code> δεν είναι εγγράψιμο.",
        "config-sqlite-cant-create-db": "Δεν ήταν δυνατή η δημιουργία του αρχείου βάσης δεδομένων <code>$1</code>.",
        "config-upgrade-done-no-regenerate": "Η αναβάθμιση ολοκληρώθηκε.\n\nΜπορείτε τώρα να [$1 ξεκινήσετε να χρησιμοποιείτε το wiki σας].",
index 60d1f57..75475ad 100644 (file)
@@ -22,6 +22,7 @@
        "config-page-language": "Hizkuntza",
        "config-page-welcome": "Ongi etorri MediaWikira!",
        "config-page-dbconnect": "Datu-basera konektatu",
+       "config-page-upgrade": "Oraingo instalazioa eguneratu",
        "config-page-dbsettings": "Datu-basearen ezarpenak",
        "config-page-name": "Izena",
        "config-page-options": "Aukerak",
        "config-page-complete": "Bukatua!",
        "config-page-restart": "Instalazioa berriz hasi",
        "config-page-readme": "Irakur nazazu",
+       "config-page-releasenotes": "Bertsioko oharrak",
        "config-page-copying": "Kopiatzea",
        "config-page-upgradedoc": "Eguneratu",
+       "config-page-existingwiki": "Existitzen den wikia",
        "config-restart": "Bai, berriz hasi",
        "config-sidebar": "* [//www.mediawiki.org MediaWiki nagusia]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Erabiltzaileentzako Gida]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratzaileentzako Gida]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MEG]\n----\n* <doclink href=Readme>Irakur nazazu</doclink>\n* <doclink href=ReleaseNotes>Oharren argitalpena</doclink>\n* <doclink href=Copying>Kopiaketa</doclink>\n* <doclink href=UpgradeDoc>Eguneratzea</doclink>",
        "config-env-php": "PHP $1 instalatuta dago.",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] instalatuta dago",
        "config-diff3-bad": "GNU diff3 ez da aurkitu.",
        "config-db-type": "Datu-base mota:",
+       "config-db-host-oracle": "Datu-baseko TNS:",
        "config-db-wiki-settings": "Wiki hau identifikatu",
        "config-db-name": "Datu-base izena:",
+       "config-db-name-oracle": "Datu-baseko eskema:",
        "config-db-username": "Datu-base lankide izena:",
        "config-db-password": "Datu-base pasahitza:",
        "config-charset-mysql5-binary": "MySQL 4.1/5.0 bitarra",
        "config-admin-password-confirm": "Pasahitza berriz:",
        "config-admin-password-mismatch": "Sartutako bi pasahitzak ez datoz bat.",
        "config-admin-email": "E-posta helbidea:",
+       "config-admin-error-bademail": "Helbide elektroniko okerra idatzi duzu.",
+       "config-optional-continue": "Galdera gehiago egin.",
+       "config-optional-skip": "Aspertuta nago, wikia instalatu bakarrik.",
        "config-profile-wiki": "Wikia ireki",
+       "config-profile-no-anon": "Kontua sortzea beharrezkoa da",
+       "config-profile-fishbowl": "Baimendutako editoreak bakarrik",
        "config-profile-private": "Wiki pribatua",
        "config-license": "Copyright eta lizentzia:",
+       "config-license-cc-by": "Creative Commons Aitorpena",
        "config-license-cc-0": "Creative Commons Zero (Jabari Publikoa)",
        "config-license-pd": "Domeinu Askea",
+       "config-license-cc-choose": "Aukeratu Creative Commons lizentzia pertsonalizatua",
        "config-email-settings": "E-posta hobespenak",
+       "config-upload-settings": "Irudi eta fitxategi igoerak",
+       "config-upload-enable": "Fitxategi igoera gaitu",
        "config-logo": "Logo URL:",
+       "config-instantcommons": "Instant Commons gaitu",
+       "config-cc-again": "Berriz aukeratu...",
        "config-advanced-settings": "Konfigurazio aurreratua",
        "config-extensions": "Luzapenak",
        "config-skins": "Itxurak",
        "config-install-step-done": "egina",
+       "config-install-extensions": "Luzapenak barne",
+       "config-install-database": "Datu-basea konfiguratu",
+       "config-install-schema": "Eskema sortu",
+       "config-install-user-alreadyexists": "\"$1\" erabiltzailea badago.",
+       "config-install-tables": "Taulak sortzen",
+       "config-install-interwiki-list": "Ezin izan da <code>interwiki.list</code> fitxategia irakurri.",
+       "config-install-stats": "Estatistikak hasten",
+       "config-install-keys": "Gako sekretuak sortzen",
+       "config-install-sysop": "Administratzaile kontua sortzen",
+       "config-download-localsettings": "Jaitsi <code>LocalSettings.php</code>",
        "config-help": "Laguntza",
+       "config-help-tooltip": "sakatu zabaltzeko",
        "mainpagetext": "'''MediaWiki arrakastaz instalatu da.'''",
        "mainpagedocfooter": "Ikus [//meta.wikimedia.org/wiki/Help:Contents Erabiltzaile Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]"
 }
index ce15eac..17b5658 100644 (file)
@@ -1,5 +1,9 @@
 {
-       "@metadata": [],
+       "@metadata": {
+               "authors": [
+                       "Snævar"
+               ]
+       },
        "mainpagetext": "'''Uppsetning á MediaWiki heppnaðist.'''",
-       "mainpagedocfooter": "Ráðfærðu þig við [//meta.wikimedia.org/wiki/Help:Contents Notandahandbókina] fyrir frekari upplýsingar um notkun wiki-hugbúnaðarins.\n\n== Fyrir byrjendur ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Listi yfir uppsetningarstillingar]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki Algengar spurningar MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Póstlisti MediaWiki-útgáfa]"
+       "mainpagedocfooter": "Ráðfærðu þig við [//meta.wikimedia.org/wiki/Help:Contents Notandahandbókina] fyrir frekari upplýsingar um notkun wiki-hugbúnaðarins.\n\n== Fyrir byrjendur ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Listi yfir uppsetningarstillingar]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki Algengar spurningar MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Póstlisti MediaWiki-útgáfa]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Læra hvernig á að berjast við amapóst á þínum wiki]"
 }
index a8cd93e..7ed7067 100644 (file)
        "config-upload-deleted": "Directory per i file cancellati:",
        "config-upload-deleted-help": "Scegli una directory in cui archiviare i file cancellati.\nIdealmente, questa non dovrebbe essere accessibile dal web.",
        "config-logo": "URL del logo:",
-       "config-logo-help": "La skin predefinita di MediaWiki include lo spazio per un logo di 135 x 160 pixel sopra il menu laterale.\nCarica un'immagine di dimensioni appropriate e inserisci l'URL qui.\n\nÈ possibile utilizzare <code>$wgStylePath</code> o <code>$wgScriptPath</code> se il logo è relativo a tali percorsi.\n\nSe non si desidera un logo, lascia vuota questa casella.",
+       "config-logo-help": "Il tema predefinito di MediaWiki include lo spazio per un logo di 135 x 160 pixel sopra il menu laterale.\nCarica un'immagine di dimensioni appropriate e inserisci l'URL qui.\n\nÈ possibile utilizzare <code>$wgStylePath</code> o <code>$wgScriptPath</code> se il logo è relativo a tali percorsi.\n\nSe non si desidera un logo, lascia vuota questa casella.",
        "config-instantcommons": "Abilita Instant Commons",
        "config-instantcommons-help": "[//www.mediawiki.org/wiki/InstantCommons Instant Commons] è una funzionalità che consente ai wiki di usare immagini, suoni e altri file multimediali che trovate sul sito [//commons.wikimedia.org/ Wikimedia Commons].\nPer fare questo, MediaWiki richiede l'accesso a Internet.\n\nPer ulteriori informazioni su questa funzionalità, incluse le istruzioni su come configurarlo per wiki diversi da Wikimedia Commons, consultare [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos il manuale].",
        "config-cc-error": "Il selettore di licenze Creative Commons non ha dato alcun risultato.\nInserisci manualmente il nome della licenza.",
        "config-memcache-badport": "I numeri di porta per memcached dovrebbero essere tra $1 e $2.",
        "config-extensions": "Estensioni",
        "config-extensions-help": "Le estensioni elencate sopra sono state rilevate nella tua directory <code>./extensions</code>.\n\nQueste potrebbero richiedere ulteriore configurazione, ma è possibile attivarle ora",
-       "config-skins": "Skin",
-       "config-skins-help": "Le skin elencate sopra sono state rilevate nella tua directory <code>./skins</code>. Devi attivarne almeno una e scegliere quella predefinita.",
-       "config-skins-use-as-default": "Usa questa skin come predefinita",
-       "config-skins-missing": "Non è stata trovata alcuna skin, MediaWiki userà una soluzione di ripiego finché non ne installerai una appropriata.",
-       "config-skins-must-enable-some": "Devi scegliere almeno una skin da attivare.",
-       "config-skins-must-enable-default": "La skin scelta come predefinita deve essere attivata.",
+       "config-skins": "Temi",
+       "config-skins-help": "I temi elencati sopra sono stati rilevati nella tua directory <code>./skins</code>. Devi attivarne almeno uno e scegliere quello predefinito.",
+       "config-skins-use-as-default": "Usa questo tema come predefinito",
+       "config-skins-missing": "Non è stato trovato alcun tema, MediaWiki userà una soluzione di ripiego finché non ne installerai uno appropriato.",
+       "config-skins-must-enable-some": "Devi scegliere almeno un tema da attivare.",
+       "config-skins-must-enable-default": "Il tema scelto come predefinito deve essere attivato.",
        "config-install-alreadydone": "'''Attenzione:''' sembra che hai già installato MediaWiki e stai tentando di installarlo nuovamente.\nProcedi alla pagina successiva.",
        "config-install-begin": "Premendo \"{{int:config-continue}}\", si avvierà l'installazione di MediaWiki.\nSe prima desideri apportare altre modifiche, premi \"{{int:config-back}}\".",
        "config-install-step-done": "fatto",
index b7187e2..5d4c5d7 100644 (file)
        "config-upload-deleted": "Папка за избришаните податотеки:",
        "config-upload-deleted-help": "Одберете во која папка да се архивираат избришаните податотеки.\nНајдобро би било ако таа не е достапна преку семрежјето.",
        "config-logo": "URL за логото:",
-       "config-logo-help": "Ð\9cаÑ\82иÑ\87ноÑ\82о Ñ\80Ñ\83во Ð½Ð° Ð\9cедиÑ\98аÐ\92ики Ð¸Ð¼Ð° Ð¿Ñ\80оÑ\81Ñ\82оÑ\80 Ð·Ð° Ð»Ð¾Ð³Ð¾ Ð¾Ð´ 135x160 Ð¿Ð¸ÐºÑ\81ели Ð½Ð°Ð´ Ñ\81Ñ\82Ñ\80аниÑ\87наÑ\82а Ð»ÐµÐ½Ñ\82а.\n\nМожете да употребите <code>$wgStylePath</code> или <code>$wgScriptPath</code> ако вашето лого е релативно на тие патеки.\n\nАко не сакате да имате лого, тогаш оставете го ова поле празно.",
+       "config-logo-help": "Ð\9cаÑ\82иÑ\87ноÑ\82о Ñ\80Ñ\83во Ð½Ð° Ð\9cедиÑ\98аÐ\92ики Ð¸Ð¼Ð° Ð¿Ñ\80оÑ\81Ñ\82оÑ\80 Ð·Ð° Ð»Ð¾Ð³Ð¾ Ð¾Ð´ 135x160 Ð¿Ð¸ÐºÑ\81ели Ð½Ð°Ð´ Ñ\81Ñ\82Ñ\80аниÑ\87никоÑ\82.\n\nМожете да употребите <code>$wgStylePath</code> или <code>$wgScriptPath</code> ако вашето лого е релативно на тие патеки.\n\nАко не сакате да имате лого, тогаш оставете го ова поле празно.",
        "config-instantcommons": "Овозможи Instant Commons",
        "config-instantcommons-help": "[//www.mediawiki.org/wiki/InstantCommons Instant Commons] е функција која им овозможува на викијата да користат слики, звучни записи и други мултимедијални содржини од [//commons.wikimedia.org/ Ризницата].\nЗа да може ова да работи, МедијаВики бара пристап до семрежјето.\n\nЗа повеќе информации за оваа функција и напатствија за нејзино поставување на вики (сите други освен Ризницата), коносултирајте го [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos прирачникот].",
        "config-cc-error": "Изборникот на лиценци од Криејтив комонс не даде резултати.\nВнесете го името на лиценцата рачно.",
diff --git a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php
new file mode 100644 (file)
index 0000000..c9e20a9
--- /dev/null
@@ -0,0 +1,234 @@
+<?php
+/**
+ * Updater for link tracking tables after a page edit.
+ *
+ * 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
+ */
+
+/**
+ * Job to add recent change entries mentioning category membership changes
+ *
+ * Parameters include:
+ *   - pageId : page ID
+ *   - revTimestamp : timestamp of the triggering revision
+ *
+ * Category changes will be mentioned for revisions at/after the timestamp for this page
+ *
+ * @since 1.27
+ */
+class CategoryMembershipChangeJob extends Job {
+       const ENQUEUE_FUDGE_SEC = 60;
+
+       public function __construct( Title $title, array $params ) {
+               parent::__construct( 'categoryMembershipChange', $title, $params );
+               // Only need one job per page. Note that ENQUEUE_FUDGE_SEC handles races where an
+               // older revision job gets inserted while the newer revision job is de-duplicated.
+               $this->removeDuplicates = true;
+       }
+
+       public function run() {
+               $page = WikiPage::newFromID( $this->params['pageId'], WikiPage::READ_LATEST );
+               if ( !$page ) {
+                       $this->setLastError( "Could not find page #{$this->params['pageId']}" );
+                       return false; // deleted?
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               // Use a named lock so that jobs for this page see each others' changes
+               $fname = __METHOD__;
+               $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
+               if ( !$dbw->lock( $lockKey, $fname, 10 ) ) {
+                       $this->setLastError( "Could not acquire lock '$lockKey'" );
+                       return false;
+               }
+
+               $unlocker = new ScopedCallback( function () use ( $dbw, $lockKey, $fname ) {
+                       $dbw->unlock( $lockKey, $fname );
+               } );
+
+               // Sanity: clear any DB transaction snapshot
+               $dbw->commit( __METHOD__, 'flush' );
+
+               $cutoffUnix = wfTimestamp( TS_UNIX, $this->params['revTimestamp'] );
+               // Using ENQUEUE_FUDGE_SEC handles jobs inserted out of revision order due to the delay
+               // between COMMIT and actual enqueueing of the CategoryMembershipChangeJob job.
+               $cutoffUnix -= self::ENQUEUE_FUDGE_SEC;
+
+               // Get the newest revision that has a SRC_CATEGORIZE row...
+               $row = $dbw->selectRow(
+                       array( 'revision', 'recentchanges' ),
+                       array( 'rev_timestamp', 'rev_id' ),
+                       array(
+                               'rev_page' => $page->getId(),
+                               'rev_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $cutoffUnix ) )
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_timestamp DESC, rev_id DESC' ),
+                       array(
+                               'recentchanges' => array(
+                                       'INNER JOIN',
+                                       array(
+                                               'rc_this_oldid = rev_id',
+                                               'rc_source' => RecentChange::SRC_CATEGORIZE,
+                                               // Allow rc_cur_id or rc_timestamp index usage
+                                               'rc_cur_id = rev_page',
+                                               'rc_timestamp >= rev_timestamp'
+                                       )
+                               )
+                       )
+               );
+               // Only consider revisions newer than any such revision
+               if ( $row ) {
+                       $cutoffUnix = wfTimestamp( TS_UNIX, $row->rev_timestamp );
+                       $lastRevId = (int)$row->rev_id;
+               } else {
+                       $lastRevId = 0;
+               }
+
+               // Find revisions to this page made around and after this revision which lack category
+               // notifications in recent changes. This lets jobs pick up were the last one left off.
+               $encCutoff = $dbw->addQuotes( $dbw->timestamp( $cutoffUnix ) );
+               $res = $dbw->select(
+                       'revision',
+                       Revision::selectFields(),
+                       array(
+                               'rev_page' => $page->getId(),
+                               "rev_timestamp > $encCutoff" .
+                                       " OR (rev_timestamp = $encCutoff AND rev_id > $lastRevId)"
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC' )
+               );
+
+               // Apply all category updates in revision timestamp order
+               foreach ( $res as $row ) {
+                       $this->notifyUpdatesForRevision( $page, Revision::newFromRow( $row ) );
+               }
+
+               ScopedCallback::consume( $unlocker );
+
+               return true;
+       }
+
+       /**
+        * @param WikiPage $page
+        * @param Revision $newRev
+        * @throws MWException
+        */
+       protected function notifyUpdatesForRevision( WikiPage $page, Revision $newRev ) {
+               $config = RequestContext::getMain()->getConfig();
+               $title = $page->getTitle();
+
+               // Get the new revision
+               if ( !$newRev->getContent() ) {
+                       return; // deleted?
+               }
+
+               // Get the prior revision (the same for null edits)
+               if ( $newRev->getParentId() ) {
+                       $oldRev = Revision::newFromId( $newRev->getParentId(), Revision::READ_LATEST );
+                       if ( !$oldRev->getContent() ) {
+                               return; // deleted?
+                       }
+               } else {
+                       $oldRev = null;
+               }
+
+               // Parse the new revision and get the categories
+               $categoryChanges = $this->getExplicitCategoriesChanges( $title, $newRev, $oldRev );
+               list( $categoryInserts, $categoryDeletes ) = $categoryChanges;
+               if ( !$categoryInserts && !$categoryDeletes ) {
+                       return; // nothing to do
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $catMembChange = new CategoryMembershipChange( $title, $newRev );
+               $catMembChange->checkTemplateLinks();
+
+               $batchSize = $config->get( 'UpdateRowsPerQuery' );
+               $insertCount = 0;
+
+               foreach ( $categoryInserts as $categoryName ) {
+                       $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
+                       $catMembChange->triggerCategoryAddedNotification( $categoryTitle );
+                       if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) {
+                               $dbw->commit( __METHOD__, 'flush' );
+                               wfWaitForSlaves();
+                       }
+               }
+
+               foreach ( $categoryDeletes as $categoryName ) {
+                       $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
+                       $catMembChange->triggerCategoryRemovedNotification( $categoryTitle );
+                       if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) {
+                               $dbw->commit( __METHOD__, 'flush' );
+                               wfWaitForSlaves();
+                       }
+               }
+       }
+
+       private function getExplicitCategoriesChanges(
+               Title $title, Revision $newRev, Revision $oldRev = null
+       ) {
+               // Inject the same timestamp for both revision parses to avoid seeing category changes
+               // due to time-based parser functions. Inject the same page title for the parses too.
+               // Note that REPEATABLE-READ makes template/file pages appear unchanged between parses.
+               $parseTimestamp = $newRev->getTimestamp();
+               // Parse the old rev and get the categories. Do not use link tables as that
+               // assumes these updates are perfectly FIFO and that link tables are always
+               // up to date, neither of which are true.
+               $oldCategories = $oldRev
+                       ? $this->getCategoriesAtRev( $title, $oldRev, $parseTimestamp )
+                       : array();
+               // Parse the new revision and get the categories
+               $newCategories = $this->getCategoriesAtRev( $title, $newRev, $parseTimestamp );
+
+               $categoryInserts = array_values( array_diff( $newCategories, $oldCategories ) );
+               $categoryDeletes = array_values( array_diff( $oldCategories, $newCategories ) );
+
+               return array( $categoryInserts, $categoryDeletes );
+       }
+
+       /**
+        * @param Title $title
+        * @param Revision $rev
+        * @param string $parseTimestamp TS_MW
+        *
+        * @return string[] category names
+        */
+       private function getCategoriesAtRev( Title $title, Revision $rev, $parseTimestamp ) {
+               $content = $rev->getContent();
+               $options = $content->getContentHandler()->makeParserOptions( 'canonical' );
+               $options->setTimestamp( $parseTimestamp );
+               // This could possibly use the parser cache if it checked the revision ID,
+               // but that's more complicated than it's worth.
+               $output = $content->getParserOutput( $title, $rev->getId(), $options );
+
+               // array keys will cast numeric category names to ints
+               // so we need to cast them back to strings to avoid breaking things!
+               return array_map( 'strval', array_keys( $output->getCategories() ) );
+       }
+
+       public function getDeduplicationInfo() {
+               $info = parent::getDeduplicationInfo();
+               unset( $info['params']['revTimestamp'] ); // first job wins
+
+               return $info;
+       }
+}
index 6b1a1e3..ae35e30 100644 (file)
@@ -29,7 +29,7 @@
  *   - a) Recursive jobs to purge caches for backlink pages for a given title.
  *        These jobs have (recursive:true,table:<table>) set.
  *   - b) Jobs to purge caches for a set of titles (the job title is ignored).
- *           These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set.
+ *        These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set.
  *
  * @ingroup JobQueue
  */
@@ -40,6 +40,23 @@ class HTMLCacheUpdateJob extends Job {
                $this->removeDuplicates = ( !isset( $params['range'] ) && !isset( $params['pages'] ) );
        }
 
+       /**
+        * @param Title $title Title to purge backlink pages from
+        * @param string $table Backlink table name
+        * @return HTMLCacheUpdateJob
+        */
+       public static function newForBacklinks( Title $title, $table ) {
+               return new self(
+                       $title,
+                       array(
+                               'table' => $table,
+                               'recursive' => true
+                       ) + Job::newRootJobParams( // "overall" refresh links job info
+                               "htmlCacheUpdate:{$table}:{$title->getPrefixedText()}"
+                       )
+               );
+       }
+
        function run() {
                global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
 
@@ -77,7 +94,7 @@ class HTMLCacheUpdateJob extends Job {
         * @param array $pages Map of (page ID => (namespace, DB key)) entries
         */
        protected function invalidateTitles( array $pages ) {
-               global $wgUpdateRowsPerQuery, $wgUseFileCache, $wgUseSquid;
+               global $wgUpdateRowsPerQuery, $wgUseFileCache;
 
                // Get all page IDs in this query into an array
                $pageIds = array_keys( $pages );
@@ -123,10 +140,8 @@ class HTMLCacheUpdateJob extends Job {
                ) );
 
                // Update squid
-               if ( $wgUseSquid ) {
-                       $u = SquidUpdate::newFromTitles( $titleArray );
-                       $u->doUpdate();
-               }
+               $u = SquidUpdate::newFromTitles( $titleArray );
+               $u->doUpdate();
 
                // Update file cache
                if ( $wgUseFileCache ) {
index 51ccf61..1770ac7 100644 (file)
@@ -92,6 +92,7 @@ class BacklinkJobUtils {
 
                if ( isset( $params['pages'] ) || empty( $params['recursive'] ) ) {
                        $ranges = array(); // sanity; this is a leaf node
+                       $realBSize = 0;
                        wfWarn( __METHOD__ . " called on {$job->getType()} leaf job (explosive recursion)." );
                } elseif ( isset( $params['range'] ) ) {
                        // This is a range job to trigger the insertion of partitioned/title jobs...
@@ -109,8 +110,10 @@ class BacklinkJobUtils {
                // Combine the first range (of size $bSize) backlinks into leaf jobs
                if ( isset( $ranges[0] ) ) {
                        list( $start, $end ) = $ranges[0];
-                       $titles = $title->getBacklinkCache()->getLinks( $params['table'], $start, $end );
-                       foreach ( array_chunk( iterator_to_array( $titles ), $cSize ) as $titleBatch ) {
+                       $iter = $title->getBacklinkCache()->getLinks( $params['table'], $start, $end );
+                       $titles = iterator_to_array( $iter );
+                       /** @var Title[] $titleBatch */
+                       foreach ( array_chunk( $titles, $cSize ) as $titleBatch ) {
                                $pages = array();
                                foreach ( $titleBatch as $tl ) {
                                        $pages[$tl->getArticleId()] = array( $tl->getNamespace(), $tl->getDBKey() );
index 16e894e..4005abb 100644 (file)
@@ -115,6 +115,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        const FLD_TTL = 2;
        const FLD_TIME = 3;
        const FLD_FLAGS = 4;
+       const FLD_HOLDOFF = 5;
 
        /** @var integer Treat this value as expired-on-arrival */
        const FLG_STALE = 1;
@@ -232,18 +233,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
                $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
 
-               $checksForAll = array();
-               $checksByKey = array();
+               $checkKeysForAll = array();
+               $checkKeysByKey = array();
                $checkKeysFlat = array();
                foreach ( $checkKeys as $i => $keys ) {
                        $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX );
                        $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
                        // Is this check keys for a specific cache key, or for all keys being fetched?
                        if ( is_int( $i ) ) {
-                               $checksForAll = array_merge( $checksForAll, $prefixed );
+                               $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
                        } else {
-                               $checksByKey[$i] = isset( $checksByKey[$i] )
-                                       ? array_merge( $checksByKey[$i], $prefixed )
+                               $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] )
+                                       ? array_merge( $checkKeysByKey[$i], $prefixed )
                                        : $prefixed;
                        }
                }
@@ -253,10 +254,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $now = microtime( true );
 
                // Collect timestamps from all "check" keys
-               $checkKeyTimesForAll = $this->processCheckKeys( $checksForAll, $wrappedValues, $now );
-               $checkKeyTimesByKey = array();
-               foreach ( $checksByKey as $cacheKey => $checks ) {
-                       $checkKeyTimesByKey[$cacheKey] =
+               $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
+               $purgeValuesByKey = array();
+               foreach ( $checkKeysByKey as $cacheKey => $checks ) {
+                       $purgeValuesByKey[$cacheKey] =
                                $this->processCheckKeys( $checks, $wrappedValues, $now );
                }
 
@@ -274,14 +275,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                                // Force dependant keys to be invalid for a while after purging
                                // to reduce race conditions involving stale data getting cached
-                               $checkKeyTimes = $checkKeyTimesForAll;
-                               if ( isset( $checkKeyTimesByKey[$key] ) ) {
-                                       $checkKeyTimes = array_merge( $checkKeyTimes, $checkKeyTimesByKey[$key] );
+                               $purgeValues = $purgeValuesForAll;
+                               if ( isset( $purgeValuesByKey[$key] ) ) {
+                                       $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
                                }
-                               foreach ( $checkKeyTimes as $checkKeyTime ) {
-                                       $safeTimestamp = $checkKeyTime + self::HOLDOFF_TTL;
+                               foreach ( $purgeValues as $purge ) {
+                                       $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
                                        if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
-                                               $curTTL = min( $curTTL, $checkKeyTime - $now );
+                                               $curTTL = min( $curTTL, $purge[self::FLD_TIME] - $now );
                                        }
                                }
                        }
@@ -296,22 +297,25 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param array $timeKeys List of prefixed time check keys
         * @param array $wrappedValues
         * @param float $now
-        * @return array List of timestamps
+        * @return array List of purge value arrays
         */
        private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
-               $times = array();
+               $purgeValues = array();
                foreach ( $timeKeys as $timeKey ) {
-                       $timestamp = isset( $wrappedValues[$timeKey] )
+                       $purge = isset( $wrappedValues[$timeKey] )
                                ? self::parsePurgeValue( $wrappedValues[$timeKey] )
                                : false;
-                       if ( !is_float( $timestamp ) ) {
+                       if ( $purge === false ) {
                                // Key is not set or invalid; regenerate
-                               $this->cache->add( $timeKey, self::PURGE_VAL_PREFIX . $now, self::CHECK_KEY_TTL );
-                               $timestamp = $now;
+                               $this->cache->add( $timeKey,
+                                       $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
+                                       self::CHECK_KEY_TTL
+                               );
+                               $purge = array( self::FLD_TIME => $now, self::FLD_HOLDOFF => self::HOLDOFF_TTL );
                        }
-                       $times[] = $timestamp;
+                       $purgeValues[] = $purge;
                }
-               return $times;
+               return $purgeValues;
        }
 
        /**
@@ -378,7 +382,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                $wrapExtra = array(); // additional wrapped value fields
                // Check if there's a risk of writing stale data after the purge tombstone expired
-               if ( ( $lag + $age ) > self::MAX_READ_LAG ) {
+               if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
                        // Case A: read lag with "lockTSE"; save but record value as stale
                        if ( $lockTSE >= 0 ) {
                                $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
@@ -389,7 +393,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                                return true; // no-op the write for being unsafe
                        // Case C: high replication lag; lower TTL instead of ignoring all set()s
-                       } elseif ( $lag > self::MAX_READ_LAG ) {
+                       } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
                                $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
                                $this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
                        // Case D: medium length request with medium replication lag; ignore this set()
@@ -479,9 +483,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        $ok = $this->relayDelete( $key ) && $ok;
                } else {
                        // Update the local datacenter immediately
-                       $ok = $this->cache->set( $key, self::PURGE_VAL_PREFIX . microtime( true ), $ttl );
+                       $ok = $this->cache->set( $key,
+                               $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+                               $ttl
+                       );
                        // Publish the purge to all datacenters
-                       $ok = $this->relayPurge( $key, $ttl ) && $ok;
+                       $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
                }
 
                return $ok;
@@ -504,17 +511,22 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * Note that "check" keys won't collide with other regular keys.
         *
         * @param string $key
-        * @return float UNIX timestamp of the key
+        * @return float UNIX timestamp of the check key
         */
        final public function getCheckKeyTime( $key ) {
                $key = self::TIME_KEY_PREFIX . $key;
 
-               $time = self::parsePurgeValue( $this->cache->get( $key ) );
-               if ( $time === false ) {
+               $purge = self::parsePurgeValue( $this->cache->get( $key ) );
+               if ( $purge !== false ) {
+                       $time = $purge[self::FLD_TIME];
+               } else {
                        // Casting assures identical floats for the next getCheckKeyTime() calls
-                       $time = (string)microtime( true );
-                       $this->cache->add( $key, self::PURGE_VAL_PREFIX . $time, self::CHECK_KEY_TTL );
-                       $time = (float)$time;
+                       $now = (string)microtime( true );
+                       $this->cache->add( $key,
+                               $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
+                               self::CHECK_KEY_TTL
+                       );
+                       $time = (float)$now;
                }
 
                return $time;
@@ -548,15 +560,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @see WANObjectCache::resetCheckKey()
         *
         * @param string $key Cache key
+        * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant
         * @return bool True if the item was purged or not found, false on failure
         */
-       final public function touchCheckKey( $key ) {
+       final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
                $key = self::TIME_KEY_PREFIX . $key;
                // Update the local datacenter immediately
                $ok = $this->cache->set( $key,
-                       self::PURGE_VAL_PREFIX . microtime( true ), self::CHECK_KEY_TTL );
+                       $this->makePurgeValue( microtime( true ), $holdoff ),
+                       self::CHECK_KEY_TTL
+               );
                // Publish the purge to all datacenters
-               return $this->relayPurge( $key, self::CHECK_KEY_TTL ) && $ok;
+               return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
        }
 
        /**
@@ -904,17 +919,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /**
         * Do the actual async bus purge of a key
         *
-        * This must set the key to "PURGED:<UNIX timestamp>"
+        * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
         *
         * @param string $key Cache key
         * @param integer $ttl How long to keep the tombstone [seconds]
+        * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
         * @return bool Success
         */
-       protected function relayPurge( $key, $ttl ) {
+       protected function relayPurge( $key, $ttl, $holdoff ) {
                $event = $this->cache->modifySimpleRelayEvent( array(
                        'cmd' => 'set',
                        'key' => $key,
-                       'val' => 'PURGED:$UNIXTIME$',
+                       'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
                        'ttl' => max( $ttl, 1 ),
                        'sbt' => true, // substitute $UNIXTIME$ with actual microtime
                ) );
@@ -996,10 +1012,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         */
        protected function unwrap( $wrapped, $now ) {
                // Check if the value is a tombstone
-               $purgeTimestamp = self::parsePurgeValue( $wrapped );
-               if ( is_float( $purgeTimestamp ) ) {
+               $purge = self::parsePurgeValue( $wrapped );
+               if ( $purge !== false ) {
                        // Purged values should always have a negative current $ttl
-                       $curTTL = min( $purgeTimestamp - $now, self::TINY_NEGATIVE );
+                       $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
                        return array( false, $curTTL );
                }
 
@@ -1042,17 +1058,36 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        }
 
        /**
-        * @param string $value String like "PURGED:<timestamp>"
-        * @return float|bool UNIX timestamp or false on failure
+        * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
+        * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
+        *  or false if value isn't a valid purge value
         */
        protected static function parsePurgeValue( $value ) {
-               $m = array();
-               if ( is_string( $value ) &&
-                       preg_match( '/^' . self::PURGE_VAL_PREFIX . '([^:]+)$/', $value, $m )
+               if ( !is_string( $value ) ) {
+                       return false;
+               }
+               $segments = explode( ':', $value, 3 );
+               if ( !isset( $segments[0] ) || !isset( $segments[1] )
+                       || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
                ) {
-                       return (float)$m[1];
-               } else {
                        return false;
                }
+               if ( !isset( $segments[2] ) ) {
+                       // Back-compat with old purge values without holdoff
+                       $segments[2] = self::HOLDOFF_TTL;
+               }
+               return array(
+                       self::FLD_TIME => (float)$segments[1],
+                       self::FLD_HOLDOFF => (int)$segments[2],
+               );
+       }
+
+       /**
+        * @param float $timestamp
+        * @param int $holdoff In seconds
+        * @return string Wrapped purge value
+        */
+       protected static function makePurgeValue( $timestamp, $holdoff ) {
+               return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
        }
 }
index 98bca05..40a98fa 100644 (file)
@@ -1027,55 +1027,6 @@ class WikiPage implements Page, IDBAccessObject {
                return new UserArrayFromResult( $res );
        }
 
-       /**
-        * Get the last N authors
-        * @param int $num Number of revisions to get
-        * @param int|string $revLatest The latest rev_id, selected from the master (optional)
-        * @return array Array of authors, duplicates not removed
-        */
-       public function getLastNAuthors( $num, $revLatest = 0 ) {
-               // First try the slave
-               // If that doesn't have the latest revision, try the master
-               $continue = 2;
-               $db = wfGetDB( DB_SLAVE );
-
-               do {
-                       $res = $db->select( array( 'page', 'revision' ),
-                               array( 'rev_id', 'rev_user_text' ),
-                               array(
-                                       'page_namespace' => $this->mTitle->getNamespace(),
-                                       'page_title' => $this->mTitle->getDBkey(),
-                                       'rev_page = page_id'
-                               ), __METHOD__,
-                               array(
-                                       'ORDER BY' => 'rev_timestamp DESC',
-                                       'LIMIT' => $num
-                               )
-                       );
-
-                       if ( !$res ) {
-                               return array();
-                       }
-
-                       $row = $db->fetchObject( $res );
-
-                       if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
-                               $db = wfGetDB( DB_MASTER );
-                               $continue--;
-                       } else {
-                               $continue = 0;
-                       }
-               } while ( $continue );
-
-               $authors = array( $row->rev_user_text );
-
-               foreach ( $res as $row ) {
-                       $authors[] = $row->rev_user_text;
-               }
-
-               return $authors;
-       }
-
        /**
         * Should the parser cache be used?
         *
@@ -1160,16 +1111,16 @@ class WikiPage implements Page, IDBAccessObject {
 
                $title = $this->mTitle;
                wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
-                       global $wgUseSquid;
                        // Invalidate the cache in auto-commit mode
                        $title->invalidateCache();
-                       if ( $wgUseSquid ) {
-                               // Send purge now that page_touched update was committed above
-                               $update = new SquidUpdate( $title->getSquidURLs() );
-                               $update->doUpdate();
-                       }
                } );
 
+               // Send purge after above page_touched update was committed
+               DeferredUpdates::addUpdate(
+                       new SquidUpdate( $title->getSquidURLs() ),
+                       DeferredUpdates::PRESEND
+               );
+
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        // @todo move this logic to MessageCache
                        if ( $this->exists() ) {
@@ -1742,6 +1693,7 @@ class WikiPage implements Page, IDBAccessObject {
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
 
+               $old_revision = $this->getRevision(); // current revision
                $old_content = $this->getContent( Revision::RAW ); // current revision's content
 
                $oldsize = $old_content ? $old_content->getSize() : 0;
@@ -1869,7 +1821,8 @@ class WikiPage implements Page, IDBAccessObject {
                                $user,
                                array(
                                        'changed' => $changed,
-                                       'oldcountable' => $oldcountable
+                                       'oldcountable' => $oldcountable,
+                                       'oldrevision' => $old_revision
                                )
                        );
 
@@ -1955,7 +1908,11 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mTimestamp = $now;
 
                        // Update links, etc.
-                       $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
+                       $this->doEditUpdates(
+                               $revision,
+                               $user,
+                               array( 'created' => true, 'oldrevision' => $old_revision )
+                       );
 
                        $hook_args = array( &$this, &$user, $content, $summary,
                                                                $flags & EDIT_MINOR, null, null, &$flags, $revision );
@@ -2156,6 +2113,8 @@ class WikiPage implements Page, IDBAccessObject {
         * - changed: boolean, whether the revision changed the content (default true)
         * - created: boolean, whether the revision created the page (default false)
         * - moved: boolean, whether the page was moved (default false)
+        * - restored: boolean, whether the page was undeleted (default false)
+        * - oldrevision: Revision object for the pre-update revision (default null)
         * - oldcountable: boolean, null, or string 'no-change' (default null):
         *   - boolean: whether the page was counted as an article before that
         *     revision, only used in changed is true and created is false
@@ -2164,10 +2123,14 @@ class WikiPage implements Page, IDBAccessObject {
         *   - 'no-change': don't update the article count, ever
         */
        public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
+               global $wgRCWatchCategoryMembership;
+
                $options += array(
                        'changed' => true,
                        'created' => false,
                        'moved' => false,
+                       'restored' => false,
+                       'oldrevision' => null,
                        'oldcountable' => null
                );
                $content = $revision->getContent();
@@ -2194,7 +2157,8 @@ class WikiPage implements Page, IDBAccessObject {
                if ( $content ) {
                        $recursive = $options['changed']; // bug 50785
                        $updates = $content->getSecondaryDataUpdates(
-                               $this->getTitle(), null, $recursive, $editInfo->output );
+                               $this->getTitle(), null, $recursive, $editInfo->output
+                       );
                        foreach ( $updates as $update ) {
                                if ( $update instanceof LinksUpdate ) {
                                        $update->setRevision( $revision );
@@ -2202,6 +2166,21 @@ class WikiPage implements Page, IDBAccessObject {
                                }
                                DeferredUpdates::addUpdate( $update );
                        }
+                       if ( $wgRCWatchCategoryMembership
+                               && ( $options['changed'] || $options['created'] )
+                               && !$options['restored']
+                       ) {
+                               // Note: jobs are pushed after deferred updates, so the job should be able to see
+                               // the recent change entry (also done via deferred updates) and carry over any
+                               // bot/deletion/IP flags, ect.
+                               JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
+                                       $this->getTitle(),
+                                       array(
+                                               'pageId' => $this->getId(),
+                                               'revTimestamp' => $revision->getTimestamp()
+                                       )
+                               ) );
+                       }
                }
 
                Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
index 2295eaa..e5f32d2 100644 (file)
@@ -108,16 +108,18 @@ class RevDelFileList extends RevDelList {
                $file = wfLocalFile( $this->title );
                $file->purgeCache();
                $file->purgeDescription();
+
+               // Purge full images from cache
                $purgeUrls = array();
                foreach ( $this->ids as $timestamp ) {
                        $archiveName = $timestamp . '!' . $this->title->getDBkey();
                        $file->purgeOldThumbnails( $archiveName );
                        $purgeUrls[] = $file->getArchiveUrl( $archiveName );
                }
-               if ( $this->getConfig()->get( 'UseSquid' ) ) {
-                       // purge full images from cache
-                       SquidUpdate::purge( $purgeUrls );
-               }
+               DeferredUpdates::addUpdate(
+                       new SquidUpdate( $purgeUrls ),
+                       DeferredUpdates::PRESEND
+               );
 
                return Status::newGood();
        }
diff --git a/includes/search/SearchExactMatchRescorer.php b/includes/search/SearchExactMatchRescorer.php
new file mode 100644 (file)
index 0000000..0ff628d
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Rescores results from a prefix search/opensearch to make sure the
+ * exact match is the first result.
+ *
+ * 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
+ */
+
+/**
+ * An utility class to rescore search results by looking for an exact match
+ * in the db and add the page found to the first position.
+ *
+ * NOTE: extracted from TitlePrefixSearch
+ * @ingroup Search
+ */
+class SearchExactMatchRescorer {
+       /**
+        * Default search backend does proper prefix searching, but custom backends
+        * may sort based on other algorithms that may cause the exact title match
+        * to not be in the results or be lower down the list.
+        * @param string $search the query
+        * @param int[] $namespaces the namespaces
+        * @param int $limit the max number of results to return
+        * @param string[] $srchres results
+        * @return string[] munged results
+        */
+       public function rescore( $search, $namespaces, $srchres, $limit ) {
+               // Pick namespace (based on PrefixSearch::defaultSearchBackend)
+               $ns = in_array( NS_MAIN, $namespaces ) ? NS_MAIN : $namespaces[0];
+               $t = Title::newFromText( $search, $ns );
+               if ( !$t || !$t->exists() ) {
+                       // No exact match so just return the search results
+                       return $srchres;
+               }
+               $string = $t->getPrefixedText();
+               $key = array_search( $string, $srchres );
+               if ( $key !== false ) {
+                       // Exact match was in the results so just move it to the front
+                       return $this->pullFront( $key, $srchres );
+               }
+               // Exact match not in the search results so check for some redirect handling cases
+               if ( $t->isRedirect() ) {
+                       $target = $this->getRedirectTarget( $t );
+                       $key = array_search( $target, $srchres );
+                       if ( $key !== false ) {
+                               // Exact match is a redirect to one of the returned matches so pull the
+                               // returned match to the front.  This might look odd but the alternative
+                               // is to put the redirect in front and drop the match.  The name of the
+                               // found match is often more descriptive/better formed than the name of
+                               // the redirect AND by definition they share a prefix.  Hopefully this
+                               // choice is less confusing and more helpful.  But it might not be.  But
+                               // it is the choice we're going with for now.
+                               return $this->pullFront( $key, $srchres );
+                       }
+                       $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
+                       if ( isset( $redirectTargetsToRedirect[$target] ) ) {
+                               // The exact match and something in the results list are both redirects
+                               // to the same thing!  In this case we'll pull the returned match to the
+                               // top following the same logic above.  Again, it might not be a perfect
+                               // choice but it'll do.
+                               return $this->pullFront( $redirectTargetsToRedirect[$target], $srchres );
+                       }
+               } else {
+                       $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
+                       if ( isset( $redirectTargetsToRedirect[$string] ) ) {
+                               // The exact match is the target of a redirect already in the results list so remove
+                               // the redirect from the results list and push the exact match to the front
+                               array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 );
+                               array_unshift( $srchres, $string );
+                               return $srchres;
+                       }
+               }
+
+               // Exact match is totally unique from the other results so just add it to the front
+               array_unshift( $srchres, $string );
+               // And roll one off the end if the results are too long
+               if ( count( $srchres ) > $limit ) {
+                       array_pop( $srchres );
+               }
+               return $srchres;
+       }
+
+       /**
+        * @param string[] $titles as strings
+        * @return array redirect target prefixedText to index of title in titles
+        *   that is a redirect to it.
+        */
+       private function redirectTargetsToRedirect( $titles ) {
+               $result = array();
+               foreach ( $titles as $key => $titleText ) {
+                       $title = Title::newFromText( $titleText );
+                       if ( !$title || !$title->isRedirect() ) {
+                               continue;
+                       }
+                       $target = $this->getRedirectTarget( $title );
+                       if ( !$target ) {
+                               continue;
+                       }
+                       $result[$target] = $key;
+               }
+               return $result;
+       }
+
+       /**
+        * Returns an array where the element of $array at index $key becomes
+        * the first element.
+        * @param int $key key to pull to the front
+        * @return array $array with the item at $key pulled to the front
+        */
+       private function pullFront( $key, $array ) {
+               $cut = array_splice( $array, $key, 1 );
+               array_unshift( $array, $cut[0] );
+               return $array;
+       }
+
+       /**
+        * Get a redirect's destination from a title
+        * @param Title $title A title to redirect. It may not redirect or even exist
+        * @return null|string If title exists and redirects, get the destination's prefixed name
+        */
+       private function getRedirectTarget( $title ) {
+               $page = WikiPage::factory( $title );
+               if ( !$page->exists() ) {
+                       return null;
+               }
+               $redir = $page->getRedirectTarget();
+               return $redir ? $redir->getPrefixedText() : null;
+       }
+}
index e796582..163f3d5 100644 (file)
@@ -675,12 +675,12 @@ class SkinTemplate extends Skin {
                                // huge loss.
                                $personal_urls['anontalk'] = array(
                                        'text' => $this->msg( 'anontalk' )->text(),
-                                       'href' => self::makeSpecialUrlSubpage( 'MyTalk', false ),
+                                       'href' => self::makeSpecialUrlSubpage( 'Mytalk', false ),
                                        'active' => false
                                );
                                $personal_urls['anoncontribs'] = array(
                                        'text' => $this->msg( 'anoncontribs' )->text(),
-                                       'href' => self::makeSpecialUrlSubpage( 'MyContributions', false ),
+                                       'href' => self::makeSpecialUrlSubpage( 'Mycontributions', false ),
                                        'active' => false
                                );
                        }
index b0def59..b702ca0 100644 (file)
@@ -220,4 +220,8 @@ class SpecialChangeContentModel extends FormSpecialPage {
                $out->setPageTitle( $this->msg( 'changecontentmodel-success-title' ) );
                $out->addWikiMsg( 'changecontentmodel-success-text', $this->title );
        }
+
+       protected function getGroupName() {
+               return 'pagetools';
+       }
 }
index 6859310..0119781 100644 (file)
@@ -228,7 +228,7 @@ class SpecialProtectedpages extends SpecialPage {
                }
 
                return '<span class="mw-input-with-label">' .
-                       Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . '&#160;' .
+                       Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . ' ' .
                        Xml::tags( 'select',
                                array( 'id' => $this->IdType, 'name' => $this->IdType ),
                                implode( "\n", $options ) ) . "</span>";
index 664205a..492db26 100644 (file)
@@ -625,11 +625,14 @@ class PageArchive {
                $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
                if ( $created || $wasnew ) {
                        // Update site stats, link tables, etc
-                       $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
                        $article->doEditUpdates(
                                $revision,
-                               $user,
-                               array( 'created' => $created, 'oldcountable' => $oldcountable )
+                               User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+                               array(
+                                       'created' => $created,
+                                       'oldcountable' => $oldcountable,
+                                       'restored' => true
+                               )
                        );
                }
 
index 32d4552..b5382a6 100644 (file)
@@ -403,8 +403,15 @@ class SpecialWatchlist extends ChangesListSpecialPage {
         */
        public function doHeader( $opts, $numRows ) {
                $user = $this->getUser();
+               $out = $this->getOutput();
 
-               $this->getOutput()->addSubtitle(
+               // if the user wishes, that the watchlist is reloaded, whenever a filter changes,
+               // add the module for that
+               if ( $user->getBoolOption( 'watchlistreloadautomatically' ) ) {
+                       $out->addModules( array( 'mediawiki.special.watchlist' ) );
+               }
+
+               $out->addSubtitle(
                        $this->msg( 'watchlistfor2', $user->getName() )
                                ->rawParams( SpecialEditWatchlist::buildTools( null ) )
                );
@@ -469,7 +476,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                $form .= $this->msg( 'watchlist-hide' ) .
                        $this->msg( 'colon-separator' )->escaped() .
                        implode( ' ', $links );
-               $form .= "\n<hr />\n<p>";
+               $form .= "\n<br />\n";
                $form .= Html::namespaceSelector(
                        array(
                                'selected' => $opts['namespace'],
@@ -495,7 +502,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        $opts['associated'],
                        array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
                ) . "</span>\n";
-               $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "</p>\n";
+               $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n";
                foreach ( $hiddenFields as $key => $value ) {
                        $form .= Html::hidden( $key, $value ) . "\n";
                }
diff --git a/includes/user/CentralIdLookup.php b/includes/user/CentralIdLookup.php
new file mode 100644 (file)
index 0000000..638a3e2
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+/**
+ * A central user id lookup service
+ *
+ * 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
+ */
+
+/**
+ * The CentralIdLookup service allows for connecting local users with
+ * cluster-wide IDs.
+ */
+abstract class CentralIdLookup implements IDBAccessObject {
+       // Audience options for accessors
+       const AUDIENCE_PUBLIC = 1;
+       const AUDIENCE_RAW = 2;
+
+       /** @var CentralIdLookup[][] */
+       private static $instances = array();
+
+       /** @var string */
+       private $providerId;
+
+       /**
+        * Fetch a CentralIdLookup
+        * @param string|null $providerId Provider ID from $wgCentralIdLookupProviders
+        * @return CentralIdLookup|null
+        */
+       public static function factory( $providerId = null ) {
+               global $wgCentralIdLookupProviders, $wgCentralIdLookupProvider;
+
+               if ( $providerId === null ) {
+                       $providerId = $wgCentralIdLookupProvider;
+               }
+
+               if ( !array_key_exists( $providerId, self::$instances ) ) {
+                       self::$instances[$providerId] = null;
+
+                       if ( isset( $wgCentralIdLookupProviders[$providerId] ) ) {
+                               $provider = ObjectFactory::getObjectFromSpec( $wgCentralIdLookupProviders[$providerId] );
+                               if ( $provider instanceof CentralIdLookup ) {
+                                       $provider->providerId = $providerId;
+                                       self::$instances[$providerId] = $provider;
+                               }
+                       }
+               }
+
+               return self::$instances[$providerId];
+       }
+
+       final public function getProviderId() {
+               return $this->providerId;
+       }
+
+       /**
+        * Check that the "audience" parameter is valid
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @return User|null User to check against, or null if no checks are needed
+        * @throws InvalidArgumentException
+        */
+       protected function checkAudience( $audience ) {
+               if ( $audience instanceof User ) {
+                       return $audience;
+               }
+               if ( $audience === self::AUDIENCE_PUBLIC ) {
+                       return new User;
+               }
+               if ( $audience === self::AUDIENCE_RAW ) {
+                       return null;
+               }
+               throw new InvalidArgumentException( 'Invalid audience' );
+       }
+
+       /**
+        * Check that a User is attached on the specified wiki.
+        *
+        * If unattached local accounts don't exist in your extension, this comes
+        * down to a check whether the central account exists at all and that
+        * $wikiId is using the same central database.
+        *
+        * @param User $user
+        * @param string|null $wikiId Wiki to check attachment status. If null, check the current wiki.
+        * @return bool
+        */
+       abstract public function isAttached( User $user, $wikiId = null );
+
+       /**
+        * Given central user IDs, return the (local) user names
+        * @note There's no requirement that the user names actually exist locally,
+        *  or if they do that they're actually attached to the central account.
+        * @param array $idToName Array with keys being central user IDs
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return array Copy of $idToName with values set to user names (or
+        *  empty-string if the user exists but $audience lacks the rights needed
+        *  to see it). IDs not corresponding to a user are unchanged.
+        */
+       abstract public function lookupCentralIds(
+               array $idToName, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       );
+
+       /**
+        * Given (local) user names, return the central IDs
+        * @note There's no requirement that the user names actually exist locally,
+        *  or if they do that they're actually attached to the central account.
+        * @param array $nameToId Array with keys being canonicalized user names
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return array Copy of $nameToId with values set to central IDs.
+        *  Names not corresponding to a user (or $audience lacks the rights needed
+        *  to see it) are unchanged.
+        */
+       abstract public function lookupUserNames(
+               array $nameToId, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       );
+
+       /**
+        * Given a central user ID, return the (local) user name
+        * @note There's no requirement that the user name actually exists locally,
+        *  or if it does that it's actually attached to the central account.
+        * @param int $id Central user ID
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return string|null User name, or empty string if $audience lacks the
+        *  rights needed to see it, or null if $id doesn't correspond to a user
+        */
+       public function nameFromCentralId(
+               $id, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               $idToName = $this->lookupCentralIds( array( $id => null ), $audience, $flags );
+               return $idToName[$id];
+       }
+
+       /**
+        * Given a (local) user name, return the central ID
+        * @note There's no requirement that the user name actually exists locally,
+        *  or if it does that it's actually attached to the central account.
+        * @param string $name Canonicalized user name
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return int User ID; 0 if the name does not correspond to a user or
+        *  $audience lacks the rights needed to see it.
+        */
+       public function centralIdFromName(
+               $name, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               $nameToId = $this->lookupUserNames( array( $name => 0 ), $audience, $flags );
+               return $nameToId[$name];
+       }
+
+       /**
+        * Given a central user ID, return a local User object
+        * @note Unlike nameFromCentralId(), this does guarantee that the local
+        *  user exists and is attached to the central account.
+        * @param int $id Central user ID
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return User|null Local user, or null if: $id doesn't correspond to a
+        *  user, $audience lacks the rights needed to see the user, the user
+        *  doesn't exist locally, or the user isn't locally attached.
+        */
+       public function localUserFromCentralId(
+               $id, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               $name = $this->nameFromCentralId( $id, $audience, $flags );
+               if ( $name !== null && $name !== '' ) {
+                       $user = User::newFromName( $name );
+                       if ( $user && $user->getId() && $this->isAttached( $user ) ) {
+                               return $user;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Given a local User object, return the central ID
+        * @note Unlike centralIdFromName(), this does guarantee that the local
+        *  user is attached to the central account.
+        * @param User $user Local user
+        * @param int|User $audience One of the audience constants, or a specific user
+        * @param int $flags IDBAccessObject read flags
+        * @return int User ID; 0 if the local user does not correspond to a
+        *  central user, $audience lacks the rights needed to see it, or the
+        *  central user isn't locally attached.
+        */
+       public function centralIdFromLocalUser(
+               User $user, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               return $this->isAttached( $user )
+                       ? $this->centralIdFromName( $user->getName(), $audience, $flags )
+                       : 0;
+       }
+
+}
diff --git a/includes/user/LocalIdLookup.php b/includes/user/LocalIdLookup.php
new file mode 100644 (file)
index 0000000..04c5b90
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+/**
+ * A central user id lookup service implementation
+ *
+ * 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
+ */
+
+/**
+ * A CentralIdLookup provider that just uses local IDs. Useful if the wiki
+ * isn't part of a cluster or you're using shared user tables.
+ *
+ * @note Shared user table support expects that all wikis involved have
+ *  $wgSharedDB and $wgSharedTables set, and that all wikis involved in the
+ *  sharing are listed in $wgLocalDatabases, and that no wikis not involved in
+ *  the sharing are listed in $wgLocalDatabases.
+ */
+class LocalIdLookup extends CentralIdLookup {
+
+       public function isAttached( User $user, $wikiId = null ) {
+               global $wgSharedDB, $wgSharedTables, $wgLocalDatabases;
+
+               // If the user has no ID, it can't be attached
+               if ( !$user->getId() ) {
+                       return false;
+               }
+
+               // Easy case, we're checking locally
+               if ( $wikiId === null || $wikiId === wfWikiId() ) {
+                       return true;
+               }
+
+               // Assume that shared user tables are set up as described above, if
+               // they're being used at all.
+               return $wgSharedDB !== null &&
+                       in_array( 'user', $wgSharedTables, true ) &&
+                       in_array( $wikiId, $wgLocalDatabases, true );
+       }
+
+       public function lookupCentralIds(
+               array $idToName, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               if ( !$idToName ) {
+                       return array();
+               }
+
+               $audience = $this->checkAudience( $audience );
+               $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+               $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
+                       ? array( 'LOCK IN SHARE MODE' )
+                       : array();
+
+               $tables = array( 'user' );
+               $fields = array( 'user_id', 'user_name' );
+               $where = array(
+                       'user_id' => array_map( 'intval', array_keys( $idToName ) ),
+               );
+               $join = array();
+               if ( $audience && !$audience->isAllowed( 'hideuser' ) ) {
+                       $tables[] = 'ipblocks';
+                       $join['ipblocks'] = array( 'LEFT JOIN', 'ipb_user=user_id' );
+                       $fields[] = 'ipb_deleted';
+               }
+
+               $res = $db->select( $tables, $fields, $where, __METHOD__, $options, $join );
+               foreach ( $res as $row ) {
+                       $idToName[$row->user_id] = empty( $row->ipb_deleted ) ? $row->user_name : '';
+               }
+
+               return $idToName;
+       }
+
+       public function lookupUserNames(
+               array $nameToId, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+       ) {
+               if ( !$nameToId ) {
+                       return array();
+               }
+
+               $audience = $this->checkAudience( $audience );
+               $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
+               $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
+                       ? array( 'LOCK IN SHARE MODE' )
+                       : array();
+
+               $tables = array( 'user' );
+               $fields = array( 'user_id', 'user_name' );
+               $where = array(
+                       'user_name' => array_map( 'strval', array_keys( $nameToId ) ),
+               );
+               $join = array();
+               if ( $audience && !$audience->isAllowed( 'hideuser' ) ) {
+                       $tables[] = 'ipblocks';
+                       $join['ipblocks'] = array( 'LEFT JOIN', 'ipb_user=user_id' );
+                       $where[] = 'ipb_deleted = 0 OR ipb_deleted IS NULL';
+               }
+
+               $res = $db->select( $tables, $fields, $where, __METHOD__, $options, $join );
+               foreach ( $res as $row ) {
+                       $nameToId[$row->user_name] = (int)$row->user_id;
+               }
+
+               return $nameToId;
+       }
+}
diff --git a/includes/user/User.php b/includes/user/User.php
new file mode 100644 (file)
index 0000000..3d1aa7e
--- /dev/null
@@ -0,0 +1,5334 @@
+<?php
+/**
+ * Implements the User class for the %MediaWiki software.
+ *
+ * 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
+ */
+
+/**
+ * String Some punctuation to prevent editing from broken text-mangling proxies.
+ * @ingroup Constants
+ */
+define( 'EDIT_TOKEN_SUFFIX', '+\\' );
+
+/**
+ * The User object encapsulates all of the user-specific settings (user_id,
+ * name, rights, email address, options, last login time). Client
+ * classes use the getXXX() functions to access these fields. These functions
+ * do all the work of determining whether the user is logged in,
+ * whether the requested option can be satisfied from cookies or
+ * whether a database query is needed. Most of the settings needed
+ * for rendering normal pages are set in the cookie to minimize use
+ * of the database.
+ */
+class User implements IDBAccessObject {
+       /**
+        * @const int Number of characters in user_token field.
+        */
+       const TOKEN_LENGTH = 32;
+
+       /**
+        * Global constant made accessible as class constants so that autoloader
+        * magic can be used.
+        */
+       const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
+
+       /**
+        * @const int Serialized record version.
+        */
+       const VERSION = 10;
+
+       /**
+        * Maximum items in $mWatchedItems
+        */
+       const MAX_WATCHED_ITEMS_CACHE = 100;
+
+       /**
+        * Exclude user options that are set to their default value.
+        * @since 1.25
+        */
+       const GETOPTIONS_EXCLUDE_DEFAULTS = 1;
+
+       /**
+        * Array of Strings List of member variables which are saved to the
+        * shared cache (memcached). Any operation which changes the
+        * corresponding database fields must call a cache-clearing function.
+        * @showinitializer
+        */
+       protected static $mCacheVars = array(
+               // user table
+               'mId',
+               'mName',
+               'mRealName',
+               'mEmail',
+               'mTouched',
+               'mToken',
+               'mEmailAuthenticated',
+               'mEmailToken',
+               'mEmailTokenExpires',
+               'mRegistration',
+               'mEditCount',
+               // user_groups table
+               'mGroups',
+               // user_properties table
+               'mOptionOverrides',
+       );
+
+       /**
+        * Array of Strings Core rights.
+        * Each of these should have a corresponding message of the form
+        * "right-$right".
+        * @showinitializer
+        */
+       protected static $mCoreRights = array(
+               'apihighlimits',
+               'applychangetags',
+               'autoconfirmed',
+               'autopatrol',
+               'bigdelete',
+               'block',
+               'blockemail',
+               'bot',
+               'browsearchive',
+               'changetags',
+               'createaccount',
+               'createpage',
+               'createtalk',
+               'delete',
+               'deletedhistory',
+               'deletedtext',
+               'deletelogentry',
+               'deleterevision',
+               'edit',
+               'editcontentmodel',
+               'editinterface',
+               'editprotected',
+               'editmyoptions',
+               'editmyprivateinfo',
+               'editmyusercss',
+               'editmyuserjs',
+               'editmywatchlist',
+               'editsemiprotected',
+               'editusercssjs', # deprecated
+               'editusercss',
+               'edituserjs',
+               'hideuser',
+               'import',
+               'importupload',
+               'ipblock-exempt',
+               'managechangetags',
+               'markbotedits',
+               'mergehistory',
+               'minoredit',
+               'move',
+               'movefile',
+               'move-categorypages',
+               'move-rootuserpages',
+               'move-subpages',
+               'nominornewtalk',
+               'noratelimit',
+               'override-export-depth',
+               'pagelang',
+               'passwordreset',
+               'patrol',
+               'patrolmarks',
+               'protect',
+               'proxyunbannable',
+               'purge',
+               'read',
+               'reupload',
+               'reupload-own',
+               'reupload-shared',
+               'rollback',
+               'sendemail',
+               'siteadmin',
+               'suppressionlog',
+               'suppressredirect',
+               'suppressrevision',
+               'unblockself',
+               'undelete',
+               'unwatchedpages',
+               'upload',
+               'upload_by_url',
+               'userrights',
+               'userrights-interwiki',
+               'viewmyprivateinfo',
+               'viewmywatchlist',
+               'viewsuppressed',
+               'writeapi',
+       );
+
+       /**
+        * String Cached results of getAllRights()
+        */
+       protected static $mAllRights = false;
+
+       /** Cache variables */
+       // @{
+       public $mId;
+       /** @var string */
+       public $mName;
+       /** @var string */
+       public $mRealName;
+
+       /** @var string */
+       public $mEmail;
+       /** @var string TS_MW timestamp from the DB */
+       public $mTouched;
+       /** @var string TS_MW timestamp from cache */
+       protected $mQuickTouched;
+       /** @var string */
+       protected $mToken;
+       /** @var string */
+       public $mEmailAuthenticated;
+       /** @var string */
+       protected $mEmailToken;
+       /** @var string */
+       protected $mEmailTokenExpires;
+       /** @var string */
+       protected $mRegistration;
+       /** @var int */
+       protected $mEditCount;
+       /** @var array */
+       public $mGroups;
+       /** @var array */
+       protected $mOptionOverrides;
+       // @}
+
+       /**
+        * Bool Whether the cache variables have been loaded.
+        */
+       // @{
+       public $mOptionsLoaded;
+
+       /**
+        * Array with already loaded items or true if all items have been loaded.
+        */
+       protected $mLoadedItems = array();
+       // @}
+
+       /**
+        * String Initialization data source if mLoadedItems!==true. May be one of:
+        *  - 'defaults'   anonymous user initialised from class defaults
+        *  - 'name'       initialise from mName
+        *  - 'id'         initialise from mId
+        *  - 'session'    log in from cookies or session if possible
+        *
+        * Use the User::newFrom*() family of functions to set this.
+        */
+       public $mFrom;
+
+       /**
+        * Lazy-initialized variables, invalidated with clearInstanceCache
+        */
+       protected $mNewtalk;
+       /** @var string */
+       protected $mDatePreference;
+       /** @var string */
+       public $mBlockedby;
+       /** @var string */
+       protected $mHash;
+       /** @var array */
+       public $mRights;
+       /** @var string */
+       protected $mBlockreason;
+       /** @var array */
+       protected $mEffectiveGroups;
+       /** @var array */
+       protected $mImplicitGroups;
+       /** @var array */
+       protected $mFormerGroups;
+       /** @var bool */
+       protected $mBlockedGlobally;
+       /** @var bool */
+       protected $mLocked;
+       /** @var bool */
+       public $mHideName;
+       /** @var array */
+       public $mOptions;
+
+       /**
+        * @var WebRequest
+        */
+       private $mRequest;
+
+       /** @var Block */
+       public $mBlock;
+
+       /** @var bool */
+       protected $mAllowUsertalk;
+
+       /** @var Block */
+       private $mBlockedFromCreateAccount = false;
+
+       /** @var array */
+       private $mWatchedItems = array();
+
+       /** @var integer User::READ_* constant bitfield used to load data */
+       protected $queryFlagsUsed = self::READ_NORMAL;
+
+       public static $idCacheByName = array();
+
+       /**
+        * Lightweight constructor for an anonymous user.
+        * Use the User::newFrom* factory functions for other kinds of users.
+        *
+        * @see newFromName()
+        * @see newFromId()
+        * @see newFromConfirmationCode()
+        * @see newFromSession()
+        * @see newFromRow()
+        */
+       public function __construct() {
+               $this->clearInstanceCache( 'defaults' );
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               return $this->getName();
+       }
+
+       /**
+        * Load the user table data for this object from the source given by mFrom.
+        *
+        * @param integer $flags User::READ_* constant bitfield
+        */
+       public function load( $flags = self::READ_NORMAL ) {
+               if ( $this->mLoadedItems === true ) {
+                       return;
+               }
+
+               // Set it now to avoid infinite recursion in accessors
+               $this->mLoadedItems = true;
+               $this->queryFlagsUsed = $flags;
+
+               switch ( $this->mFrom ) {
+                       case 'defaults':
+                               $this->loadDefaults();
+                               break;
+                       case 'name':
+                               // Make sure this thread sees its own changes
+                               if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
+                                       $flags |= self::READ_LATEST;
+                                       $this->queryFlagsUsed = $flags;
+                               }
+
+                               $this->mId = self::idFromName( $this->mName, $flags );
+                               if ( !$this->mId ) {
+                                       // Nonexistent user placeholder object
+                                       $this->loadDefaults( $this->mName );
+                               } else {
+                                       $this->loadFromId( $flags );
+                               }
+                               break;
+                       case 'id':
+                               $this->loadFromId( $flags );
+                               break;
+                       case 'session':
+                               if ( !$this->loadFromSession() ) {
+                                       // Loading from session failed. Load defaults.
+                                       $this->loadDefaults();
+                               }
+                               Hooks::run( 'UserLoadAfterLoadFromSession', array( $this ) );
+                               break;
+                       default:
+                               throw new UnexpectedValueException(
+                                       "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
+               }
+       }
+
+       /**
+        * Load user table data, given mId has already been set.
+        * @param integer $flags User::READ_* constant bitfield
+        * @return bool False if the ID does not exist, true otherwise
+        */
+       public function loadFromId( $flags = self::READ_NORMAL ) {
+               if ( $this->mId == 0 ) {
+                       $this->loadDefaults();
+                       return false;
+               }
+
+               // Try cache (unless this needs data from the master DB).
+               // NOTE: if this thread called saveSettings(), the cache was cleared.
+               $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
+               if ( $latest || !$this->loadFromCache() ) {
+                       wfDebug( "User: cache miss for user {$this->mId}\n" );
+                       // Load from DB (make sure this thread sees its own changes)
+                       if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
+                               $flags |= self::READ_LATEST;
+                       }
+                       if ( !$this->loadFromDatabase( $flags ) ) {
+                               // Can't load from ID, user is anonymous
+                               return false;
+                       }
+                       $this->saveToCache();
+               }
+
+               $this->mLoadedItems = true;
+               $this->queryFlagsUsed = $flags;
+
+               return true;
+       }
+
+       /**
+        * @since 1.27
+        * @param string $wikiId
+        * @param integer $userId
+        */
+       public static function purge( $wikiId, $userId ) {
+               $cache = ObjectCache::getMainWANInstance();
+               $cache->delete( $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId ) );
+       }
+
+       /**
+        * @since 1.27
+        * @param WANObjectCache $cache
+        * @return string
+        */
+       protected function getCacheKey( WANObjectCache $cache ) {
+               return $cache->makeGlobalKey( 'user', 'id', wfWikiID(), $this->mId );
+       }
+
+       /**
+        * Load user data from shared cache, given mId has already been set.
+        *
+        * @return bool false if the ID does not exist or data is invalid, true otherwise
+        * @since 1.25
+        */
+       protected function loadFromCache() {
+               if ( $this->mId == 0 ) {
+                       $this->loadDefaults();
+                       return false;
+               }
+
+               $cache = ObjectCache::getMainWANInstance();
+               $data = $cache->get( $this->getCacheKey( $cache ) );
+               if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) {
+                       // Object is expired
+                       return false;
+               }
+
+               wfDebug( "User: got user {$this->mId} from cache\n" );
+
+               // Restore from cache
+               foreach ( self::$mCacheVars as $name ) {
+                       $this->$name = $data[$name];
+               }
+
+               return true;
+       }
+
+       /**
+        * Save user data to the shared cache
+        *
+        * This method should not be called outside the User class
+        */
+       public function saveToCache() {
+               $this->load();
+               $this->loadGroups();
+               $this->loadOptions();
+
+               if ( $this->isAnon() ) {
+                       // Anonymous users are uncached
+                       return;
+               }
+
+               $data = array();
+               foreach ( self::$mCacheVars as $name ) {
+                       $data[$name] = $this->$name;
+               }
+               $data['mVersion'] = self::VERSION;
+               $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+
+               $cache = ObjectCache::getMainWANInstance();
+               $key = $this->getCacheKey( $cache );
+               $cache->set( $key, $data, $cache::TTL_HOUR, $opts );
+       }
+
+       /** @name newFrom*() static factory methods */
+       // @{
+
+       /**
+        * Static factory method for creation from username.
+        *
+        * This is slightly less efficient than newFromId(), so use newFromId() if
+        * you have both an ID and a name handy.
+        *
+        * @param string $name Username, validated by Title::newFromText()
+        * @param string|bool $validate Validate username. Takes the same parameters as
+        *  User::getCanonicalName(), except that true is accepted as an alias
+        *  for 'valid', for BC.
+        *
+        * @return User|bool User object, or false if the username is invalid
+        *  (e.g. if it contains illegal characters or is an IP address). If the
+        *  username is not present in the database, the result will be a user object
+        *  with a name, zero user ID and default settings.
+        */
+       public static function newFromName( $name, $validate = 'valid' ) {
+               if ( $validate === true ) {
+                       $validate = 'valid';
+               }
+               $name = self::getCanonicalName( $name, $validate );
+               if ( $name === false ) {
+                       return false;
+               } else {
+                       // Create unloaded user object
+                       $u = new User;
+                       $u->mName = $name;
+                       $u->mFrom = 'name';
+                       $u->setItemLoaded( 'name' );
+                       return $u;
+               }
+       }
+
+       /**
+        * Static factory method for creation from a given user ID.
+        *
+        * @param int $id Valid user ID
+        * @return User The corresponding User object
+        */
+       public static function newFromId( $id ) {
+               $u = new User;
+               $u->mId = $id;
+               $u->mFrom = 'id';
+               $u->setItemLoaded( 'id' );
+               return $u;
+       }
+
+       /**
+        * Factory method to fetch whichever user has a given email confirmation code.
+        * This code is generated when an account is created or its e-mail address
+        * has changed.
+        *
+        * If the code is invalid or has expired, returns NULL.
+        *
+        * @param string $code Confirmation code
+        * @param int $flags User::READ_* bitfield
+        * @return User|null
+        */
+       public static function newFromConfirmationCode( $code, $flags = 0 ) {
+               $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+
+               $id = $db->selectField(
+                       'user',
+                       'user_id',
+                       array(
+                               'user_email_token' => md5( $code ),
+                               'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
+                       )
+               );
+
+               return $id ? User::newFromId( $id ) : null;
+       }
+
+       /**
+        * Create a new user object using data from session or cookies. If the
+        * login credentials are invalid, the result is an anonymous user.
+        *
+        * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
+        * @return User
+        */
+       public static function newFromSession( WebRequest $request = null ) {
+               $user = new User;
+               $user->mFrom = 'session';
+               $user->mRequest = $request;
+               return $user;
+       }
+
+       /**
+        * Create a new user object from a user row.
+        * The row should have the following fields from the user table in it:
+        * - either user_name or user_id to load further data if needed (or both)
+        * - user_real_name
+        * - all other fields (email, etc.)
+        * It is useless to provide the remaining fields if either user_id,
+        * user_name and user_real_name are not provided because the whole row
+        * will be loaded once more from the database when accessing them.
+        *
+        * @param stdClass $row A row from the user table
+        * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
+        * @return User
+        */
+       public static function newFromRow( $row, $data = null ) {
+               $user = new User;
+               $user->loadFromRow( $row, $data );
+               return $user;
+       }
+
+       /**
+        * Static factory method for creation of a "system" user from username.
+        *
+        * A "system" user is an account that's used to attribute logged actions
+        * taken by MediaWiki itself, as opposed to a bot or human user. Examples
+        * might include the 'Maintenance script' or 'Conversion script' accounts
+        * used by various scripts in the maintenance/ directory or accounts such
+        * as 'MediaWiki message delivery' used by the MassMessage extension.
+        *
+        * This can optionally create the user if it doesn't exist, and "steal" the
+        * account if it does exist.
+        *
+        * @param string $name Username
+        * @param array $options Options are:
+        *  - validate: As for User::getCanonicalName(), default 'valid'
+        *  - create: Whether to create the user if it doesn't already exist, default true
+        *  - steal: Whether to reset the account's password and email if it
+        *    already exists, default false
+        * @return User|null
+        */
+       public static function newSystemUser( $name, $options = array() ) {
+               $options += array(
+                       'validate' => 'valid',
+                       'create' => true,
+                       'steal' => false,
+               );
+
+               $name = self::getCanonicalName( $name, $options['validate'] );
+               if ( $name === false ) {
+                       return null;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $row = $dbw->selectRow(
+                       'user',
+                       array_merge(
+                               self::selectFields(),
+                               array( 'user_password', 'user_newpassword' )
+                       ),
+                       array( 'user_name' => $name ),
+                       __METHOD__
+               );
+               if ( !$row ) {
+                       // No user. Create it?
+                       return $options['create'] ? self::createNew( $name ) : null;
+               }
+               $user = self::newFromRow( $row );
+
+               // A user is considered to exist as a non-system user if it has a
+               // password set, or a temporary password set, or an email set.
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               try {
+                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
+               } catch ( PasswordError $e ) {
+                       wfDebug( 'Invalid password hash found in database.' );
+                       $password = PasswordFactory::newInvalidPassword();
+               }
+               try {
+                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+               } catch ( PasswordError $e ) {
+                       wfDebug( 'Invalid password hash found in database.' );
+                       $newpassword = PasswordFactory::newInvalidPassword();
+               }
+               if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
+                       || $user->mEmail
+               ) {
+                       // User exists. Steal it?
+                       if ( !$options['steal'] ) {
+                               return null;
+                       }
+
+                       $nopass = PasswordFactory::newInvalidPassword()->toString();
+
+                       $dbw->update(
+                               'user',
+                               array(
+                                       'user_password' => $nopass,
+                                       'user_newpassword' => $nopass,
+                                       'user_newpass_time' => null,
+                               ),
+                               array( 'user_id' => $user->getId() ),
+                               __METHOD__
+                       );
+                       $user->invalidateEmail();
+                       $user->saveSettings();
+               }
+
+               return $user;
+       }
+
+       // @}
+
+       /**
+        * Get the username corresponding to a given user ID
+        * @param int $id User ID
+        * @return string|bool The corresponding username
+        */
+       public static function whoIs( $id ) {
+               return UserCache::singleton()->getProp( $id, 'name' );
+       }
+
+       /**
+        * Get the real name of a user given their user ID
+        *
+        * @param int $id User ID
+        * @return string|bool The corresponding user's real name
+        */
+       public static function whoIsReal( $id ) {
+               return UserCache::singleton()->getProp( $id, 'real_name' );
+       }
+
+       /**
+        * Get database id given a user name
+        * @param string $name Username
+        * @param integer $flags User::READ_* constant bitfield
+        * @return int|null The corresponding user's ID, or null if user is nonexistent
+        */
+       public static function idFromName( $name, $flags = self::READ_NORMAL ) {
+               $nt = Title::makeTitleSafe( NS_USER, $name );
+               if ( is_null( $nt ) ) {
+                       // Illegal name
+                       return null;
+               }
+
+               if ( !( $flags & self::READ_LATEST ) && isset( self::$idCacheByName[$name] ) ) {
+                       return self::$idCacheByName[$name];
+               }
+
+               $db = ( $flags & self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+
+               $s = $db->selectRow(
+                       'user',
+                       array( 'user_id' ),
+                       array( 'user_name' => $nt->getText() ),
+                       __METHOD__
+               );
+
+               if ( $s === false ) {
+                       $result = null;
+               } else {
+                       $result = $s->user_id;
+               }
+
+               self::$idCacheByName[$name] = $result;
+
+               if ( count( self::$idCacheByName ) > 1000 ) {
+                       self::$idCacheByName = array();
+               }
+
+               return $result;
+       }
+
+       /**
+        * Reset the cache used in idFromName(). For use in tests.
+        */
+       public static function resetIdByNameCache() {
+               self::$idCacheByName = array();
+       }
+
+       /**
+        * Does the string match an anonymous IPv4 address?
+        *
+        * This function exists for username validation, in order to reject
+        * usernames which are similar in form to IP addresses. Strings such
+        * as 300.300.300.300 will return true because it looks like an IP
+        * address, despite not being strictly valid.
+        *
+        * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
+        * address because the usemod software would "cloak" anonymous IP
+        * addresses like this, if we allowed accounts like this to be created
+        * new users could get the old edits of these anonymous users.
+        *
+        * @param string $name Name to match
+        * @return bool
+        */
+       public static function isIP( $name ) {
+               return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
+                       || IP::isIPv6( $name );
+       }
+
+       /**
+        * Is the input a valid username?
+        *
+        * Checks if the input is a valid username, we don't want an empty string,
+        * an IP address, anything that contains slashes (would mess up subpages),
+        * is longer than the maximum allowed username size or doesn't begin with
+        * a capital letter.
+        *
+        * @param string $name Name to match
+        * @return bool
+        */
+       public static function isValidUserName( $name ) {
+               global $wgContLang, $wgMaxNameChars;
+
+               if ( $name == ''
+                       || User::isIP( $name )
+                       || strpos( $name, '/' ) !== false
+                       || strlen( $name ) > $wgMaxNameChars
+                       || $name != $wgContLang->ucfirst( $name )
+               ) {
+                       wfDebugLog( 'username', __METHOD__ .
+                               ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
+                       return false;
+               }
+
+               // Ensure that the name can't be misresolved as a different title,
+               // such as with extra namespace keys at the start.
+               $parsed = Title::newFromText( $name );
+               if ( is_null( $parsed )
+                       || $parsed->getNamespace()
+                       || strcmp( $name, $parsed->getPrefixedText() ) ) {
+                       wfDebugLog( 'username', __METHOD__ .
+                               ": '$name' invalid due to ambiguous prefixes" );
+                       return false;
+               }
+
+               // Check an additional blacklist of troublemaker characters.
+               // Should these be merged into the title char list?
+               $unicodeBlacklist = '/[' .
+                       '\x{0080}-\x{009f}' . # iso-8859-1 control chars
+                       '\x{00a0}' .          # non-breaking space
+                       '\x{2000}-\x{200f}' . # various whitespace
+                       '\x{2028}-\x{202f}' . # breaks and control chars
+                       '\x{3000}' .          # ideographic space
+                       '\x{e000}-\x{f8ff}' . # private use
+                       ']/u';
+               if ( preg_match( $unicodeBlacklist, $name ) ) {
+                       wfDebugLog( 'username', __METHOD__ .
+                               ": '$name' invalid due to blacklisted characters" );
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Usernames which fail to pass this function will be blocked
+        * from user login and new account registrations, but may be used
+        * internally by batch processes.
+        *
+        * If an account already exists in this form, login will be blocked
+        * by a failure to pass this function.
+        *
+        * @param string $name Name to match
+        * @return bool
+        */
+       public static function isUsableName( $name ) {
+               global $wgReservedUsernames;
+               // Must be a valid username, obviously ;)
+               if ( !self::isValidUserName( $name ) ) {
+                       return false;
+               }
+
+               static $reservedUsernames = false;
+               if ( !$reservedUsernames ) {
+                       $reservedUsernames = $wgReservedUsernames;
+                       Hooks::run( 'UserGetReservedNames', array( &$reservedUsernames ) );
+               }
+
+               // Certain names may be reserved for batch processes.
+               foreach ( $reservedUsernames as $reserved ) {
+                       if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
+                               $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
+                       }
+                       if ( $reserved == $name ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Usernames which fail to pass this function will be blocked
+        * from new account registrations, but may be used internally
+        * either by batch processes or by user accounts which have
+        * already been created.
+        *
+        * Additional blacklisting may be added here rather than in
+        * isValidUserName() to avoid disrupting existing accounts.
+        *
+        * @param string $name String to match
+        * @return bool
+        */
+       public static function isCreatableName( $name ) {
+               global $wgInvalidUsernameCharacters;
+
+               // Ensure that the username isn't longer than 235 bytes, so that
+               // (at least for the builtin skins) user javascript and css files
+               // will work. (bug 23080)
+               if ( strlen( $name ) > 235 ) {
+                       wfDebugLog( 'username', __METHOD__ .
+                               ": '$name' invalid due to length" );
+                       return false;
+               }
+
+               // Preg yells if you try to give it an empty string
+               if ( $wgInvalidUsernameCharacters !== '' ) {
+                       if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
+                               wfDebugLog( 'username', __METHOD__ .
+                                       ": '$name' invalid due to wgInvalidUsernameCharacters" );
+                               return false;
+                       }
+               }
+
+               return self::isUsableName( $name );
+       }
+
+       /**
+        * Is the input a valid password for this user?
+        *
+        * @param string $password Desired password
+        * @return bool
+        */
+       public function isValidPassword( $password ) {
+               // simple boolean wrapper for getPasswordValidity
+               return $this->getPasswordValidity( $password ) === true;
+       }
+
+
+       /**
+        * Given unvalidated password input, return error message on failure.
+        *
+        * @param string $password Desired password
+        * @return bool|string|array True on success, string or array of error message on failure
+        */
+       public function getPasswordValidity( $password ) {
+               $result = $this->checkPasswordValidity( $password );
+               if ( $result->isGood() ) {
+                       return true;
+               } else {
+                       $messages = array();
+                       foreach ( $result->getErrorsByType( 'error' ) as $error ) {
+                               $messages[] = $error['message'];
+                       }
+                       foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
+                               $messages[] = $warning['message'];
+                       }
+                       if ( count( $messages ) === 1 ) {
+                               return $messages[0];
+                       }
+                       return $messages;
+               }
+       }
+
+       /**
+        * Check if this is a valid password for this user
+        *
+        * Create a Status object based on the password's validity.
+        * The Status should be set to fatal if the user should not
+        * be allowed to log in, and should have any errors that
+        * would block changing the password.
+        *
+        * If the return value of this is not OK, the password
+        * should not be checked. If the return value is not Good,
+        * the password can be checked, but the user should not be
+        * able to set their password to this.
+        *
+        * @param string $password Desired password
+        * @param string $purpose one of 'login', 'create', 'reset'
+        * @return Status
+        * @since 1.23
+        */
+       public function checkPasswordValidity( $password, $purpose = 'login' ) {
+               global $wgPasswordPolicy;
+
+               $upp = new UserPasswordPolicy(
+                       $wgPasswordPolicy['policies'],
+                       $wgPasswordPolicy['checks']
+               );
+
+               $status = Status::newGood();
+               $result = false; // init $result to false for the internal checks
+
+               if ( !Hooks::run( 'isValidPassword', array( $password, &$result, $this ) ) ) {
+                       $status->error( $result );
+                       return $status;
+               }
+
+               if ( $result === false ) {
+                       $status->merge( $upp->checkUserPassword( $this, $password, $purpose ) );
+                       return $status;
+               } elseif ( $result === true ) {
+                       return $status;
+               } else {
+                       $status->error( $result );
+                       return $status; // the isValidPassword hook set a string $result and returned true
+               }
+       }
+
+       /**
+        * Given unvalidated user input, return a canonical username, or false if
+        * the username is invalid.
+        * @param string $name User input
+        * @param string|bool $validate Type of validation to use:
+        *   - false        No validation
+        *   - 'valid'      Valid for batch processes
+        *   - 'usable'     Valid for batch processes and login
+        *   - 'creatable'  Valid for batch processes, login and account creation
+        *
+        * @throws InvalidArgumentException
+        * @return bool|string
+        */
+       public static function getCanonicalName( $name, $validate = 'valid' ) {
+               // Force usernames to capital
+               global $wgContLang;
+               $name = $wgContLang->ucfirst( $name );
+
+               # Reject names containing '#'; these will be cleaned up
+               # with title normalisation, but then it's too late to
+               # check elsewhere
+               if ( strpos( $name, '#' ) !== false ) {
+                       return false;
+               }
+
+               // Clean up name according to title rules,
+               // but only when validation is requested (bug 12654)
+               $t = ( $validate !== false ) ?
+                       Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
+               // Check for invalid titles
+               if ( is_null( $t ) ) {
+                       return false;
+               }
+
+               // Reject various classes of invalid names
+               global $wgAuth;
+               $name = $wgAuth->getCanonicalName( $t->getText() );
+
+               switch ( $validate ) {
+                       case false:
+                               break;
+                       case 'valid':
+                               if ( !User::isValidUserName( $name ) ) {
+                                       $name = false;
+                               }
+                               break;
+                       case 'usable':
+                               if ( !User::isUsableName( $name ) ) {
+                                       $name = false;
+                               }
+                               break;
+                       case 'creatable':
+                               if ( !User::isCreatableName( $name ) ) {
+                                       $name = false;
+                               }
+                               break;
+                       default:
+                               throw new InvalidArgumentException(
+                                       'Invalid parameter value for $validate in ' . __METHOD__ );
+               }
+               return $name;
+       }
+
+       /**
+        * Count the number of edits of a user
+        *
+        * @param int $uid User ID to check
+        * @return int The user's edit count
+        *
+        * @deprecated since 1.21 in favour of User::getEditCount
+        */
+       public static function edits( $uid ) {
+               wfDeprecated( __METHOD__, '1.21' );
+               $user = self::newFromId( $uid );
+               return $user->getEditCount();
+       }
+
+       /**
+        * Return a random password.
+        *
+        * @deprecated since 1.27, use PasswordFactory::generateRandomPasswordString()
+        * @return string New random password
+        */
+       public static function randomPassword() {
+               global $wgMinimalPasswordLength;
+               return PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
+       }
+
+       /**
+        * Set cached properties to default.
+        *
+        * @note This no longer clears uncached lazy-initialised properties;
+        *       the constructor does that instead.
+        *
+        * @param string|bool $name
+        */
+       public function loadDefaults( $name = false ) {
+               $this->mId = 0;
+               $this->mName = $name;
+               $this->mRealName = '';
+               $this->mEmail = '';
+               $this->mOptionOverrides = null;
+               $this->mOptionsLoaded = false;
+
+               $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
+               if ( $loggedOut !== null ) {
+                       $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
+               } else {
+                       $this->mTouched = '1'; # Allow any pages to be cached
+               }
+
+               $this->mToken = null; // Don't run cryptographic functions till we need a token
+               $this->mEmailAuthenticated = null;
+               $this->mEmailToken = '';
+               $this->mEmailTokenExpires = null;
+               $this->mRegistration = wfTimestamp( TS_MW );
+               $this->mGroups = array();
+
+               Hooks::run( 'UserLoadDefaults', array( $this, $name ) );
+       }
+
+       /**
+        * Return whether an item has been loaded.
+        *
+        * @param string $item Item to check. Current possibilities:
+        *   - id
+        *   - name
+        *   - realname
+        * @param string $all 'all' to check if the whole object has been loaded
+        *   or any other string to check if only the item is available (e.g.
+        *   for optimisation)
+        * @return bool
+        */
+       public function isItemLoaded( $item, $all = 'all' ) {
+               return ( $this->mLoadedItems === true && $all === 'all' ) ||
+                       ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
+       }
+
+       /**
+        * Set that an item has been loaded
+        *
+        * @param string $item
+        */
+       protected function setItemLoaded( $item ) {
+               if ( is_array( $this->mLoadedItems ) ) {
+                       $this->mLoadedItems[$item] = true;
+               }
+       }
+
+       /**
+        * Load user data from the session or login cookie.
+        *
+        * @return bool True if the user is logged in, false otherwise.
+        */
+       private function loadFromSession() {
+               $result = null;
+               Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
+               if ( $result !== null ) {
+                       return $result;
+               }
+
+               $request = $this->getRequest();
+
+               $cookieId = $request->getCookie( 'UserID' );
+               $sessId = $request->getSessionData( 'wsUserID' );
+
+               if ( $cookieId !== null ) {
+                       $sId = intval( $cookieId );
+                       if ( $sessId !== null && $cookieId != $sessId ) {
+                               wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
+                                       cookie user ID ($sId) don't match!" );
+                               return false;
+                       }
+                       $request->setSessionData( 'wsUserID', $sId );
+               } elseif ( $sessId !== null && $sessId != 0 ) {
+                       $sId = $sessId;
+               } else {
+                       return false;
+               }
+
+               if ( $request->getSessionData( 'wsUserName' ) !== null ) {
+                       $sName = $request->getSessionData( 'wsUserName' );
+               } elseif ( $request->getCookie( 'UserName' ) !== null ) {
+                       $sName = $request->getCookie( 'UserName' );
+                       $request->setSessionData( 'wsUserName', $sName );
+               } else {
+                       return false;
+               }
+
+               $proposedUser = User::newFromId( $sId );
+               if ( !$proposedUser->isLoggedIn() ) {
+                       // Not a valid ID
+                       return false;
+               }
+
+               global $wgBlockDisablesLogin;
+               if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
+                       // User blocked and we've disabled blocked user logins
+                       return false;
+               }
+
+               if ( $request->getSessionData( 'wsToken' ) ) {
+                       $passwordCorrect =
+                               ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
+                       $from = 'session';
+               } elseif ( $request->getCookie( 'Token' ) ) {
+                       # Get the token from DB/cache and clean it up to remove garbage padding.
+                       # This deals with historical problems with bugs and the default column value.
+                       $token = rtrim( $proposedUser->getToken( false ) ); // correct token
+                       // Make comparison in constant time (bug 61346)
+                       $passwordCorrect = strlen( $token )
+                               && hash_equals( $token, $request->getCookie( 'Token' ) );
+                       $from = 'cookie';
+               } else {
+                       // No session or persistent login cookie
+                       return false;
+               }
+
+               if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
+                       $this->loadFromUserObject( $proposedUser );
+                       $request->setSessionData( 'wsToken', $this->mToken );
+                       wfDebug( "User: logged in from $from\n" );
+                       return true;
+               } else {
+                       // Invalid credentials
+                       wfDebug( "User: can't log in from $from, invalid credentials\n" );
+                       return false;
+               }
+       }
+
+       /**
+        * Load user and user_group data from the database.
+        * $this->mId must be set, this is how the user is identified.
+        *
+        * @param integer $flags User::READ_* constant bitfield
+        * @return bool True if the user exists, false if the user is anonymous
+        */
+       public function loadFromDatabase( $flags = self::READ_LATEST ) {
+               // Paranoia
+               $this->mId = intval( $this->mId );
+
+               // Anonymous user
+               if ( !$this->mId ) {
+                       $this->loadDefaults();
+                       return false;
+               }
+
+               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+               $db = wfGetDB( $index );
+
+               $s = $db->selectRow(
+                       'user',
+                       self::selectFields(),
+                       array( 'user_id' => $this->mId ),
+                       __METHOD__,
+                       $options
+               );
+
+               $this->queryFlagsUsed = $flags;
+               Hooks::run( 'UserLoadFromDatabase', array( $this, &$s ) );
+
+               if ( $s !== false ) {
+                       // Initialise user table data
+                       $this->loadFromRow( $s );
+                       $this->mGroups = null; // deferred
+                       $this->getEditCount(); // revalidation for nulls
+                       return true;
+               } else {
+                       // Invalid user_id
+                       $this->mId = 0;
+                       $this->loadDefaults();
+                       return false;
+               }
+       }
+
+       /**
+        * Initialize this object from a row from the user table.
+        *
+        * @param stdClass $row Row from the user table to load.
+        * @param array $data Further user data to load into the object
+        *
+        *      user_groups             Array with groups out of the user_groups table
+        *      user_properties         Array with properties out of the user_properties table
+        */
+       protected function loadFromRow( $row, $data = null ) {
+               $all = true;
+
+               $this->mGroups = null; // deferred
+
+               if ( isset( $row->user_name ) ) {
+                       $this->mName = $row->user_name;
+                       $this->mFrom = 'name';
+                       $this->setItemLoaded( 'name' );
+               } else {
+                       $all = false;
+               }
+
+               if ( isset( $row->user_real_name ) ) {
+                       $this->mRealName = $row->user_real_name;
+                       $this->setItemLoaded( 'realname' );
+               } else {
+                       $all = false;
+               }
+
+               if ( isset( $row->user_id ) ) {
+                       $this->mId = intval( $row->user_id );
+                       $this->mFrom = 'id';
+                       $this->setItemLoaded( 'id' );
+               } else {
+                       $all = false;
+               }
+
+               if ( isset( $row->user_id ) && isset( $row->user_name ) ) {
+                       self::$idCacheByName[$row->user_name] = $row->user_id;
+               }
+
+               if ( isset( $row->user_editcount ) ) {
+                       $this->mEditCount = $row->user_editcount;
+               } else {
+                       $all = false;
+               }
+
+               if ( isset( $row->user_email ) ) {
+                       $this->mEmail = $row->user_email;
+                       $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
+                       $this->mToken = $row->user_token;
+                       if ( $this->mToken == '' ) {
+                               $this->mToken = null;
+                       }
+                       $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
+                       $this->mEmailToken = $row->user_email_token;
+                       $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
+                       $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
+               } else {
+                       $all = false;
+               }
+
+               if ( $all ) {
+                       $this->mLoadedItems = true;
+               }
+
+               if ( is_array( $data ) ) {
+                       if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
+                               $this->mGroups = $data['user_groups'];
+                       }
+                       if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
+                               $this->loadOptions( $data['user_properties'] );
+                       }
+               }
+       }
+
+       /**
+        * Load the data for this user object from another user object.
+        *
+        * @param User $user
+        */
+       protected function loadFromUserObject( $user ) {
+               $user->load();
+               $user->loadGroups();
+               $user->loadOptions();
+               foreach ( self::$mCacheVars as $var ) {
+                       $this->$var = $user->$var;
+               }
+       }
+
+       /**
+        * Load the groups from the database if they aren't already loaded.
+        */
+       private function loadGroups() {
+               if ( is_null( $this->mGroups ) ) {
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
+                       $res = $db->select( 'user_groups',
+                               array( 'ug_group' ),
+                               array( 'ug_user' => $this->mId ),
+                               __METHOD__ );
+                       $this->mGroups = array();
+                       foreach ( $res as $row ) {
+                               $this->mGroups[] = $row->ug_group;
+                       }
+               }
+       }
+
+       /**
+        * Add the user to the group if he/she meets given criteria.
+        *
+        * Contrary to autopromotion by \ref $wgAutopromote, the group will be
+        *   possible to remove manually via Special:UserRights. In such case it
+        *   will not be re-added automatically. The user will also not lose the
+        *   group if they no longer meet the criteria.
+        *
+        * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria)
+        *
+        * @return array Array of groups the user has been promoted to.
+        *
+        * @see $wgAutopromoteOnce
+        */
+       public function addAutopromoteOnceGroups( $event ) {
+               global $wgAutopromoteOnceLogInRC, $wgAuth;
+
+               if ( wfReadOnly() || !$this->getId() ) {
+                       return array();
+               }
+
+               $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
+               if ( !count( $toPromote ) ) {
+                       return array();
+               }
+
+               if ( !$this->checkAndSetTouched() ) {
+                       return array(); // raced out (bug T48834)
+               }
+
+               $oldGroups = $this->getGroups(); // previous groups
+               foreach ( $toPromote as $group ) {
+                       $this->addGroup( $group );
+               }
+               // update groups in external authentication database
+               Hooks::run( 'UserGroupsChanged', array( $this, $toPromote, array(), false ) );
+               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+
+               $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+
+               $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
+               $logEntry->setPerformer( $this );
+               $logEntry->setTarget( $this->getUserPage() );
+               $logEntry->setParameters( array(
+                       '4::oldgroups' => $oldGroups,
+                       '5::newgroups' => $newGroups,
+               ) );
+               $logid = $logEntry->insert();
+               if ( $wgAutopromoteOnceLogInRC ) {
+                       $logEntry->publish( $logid );
+               }
+
+               return $toPromote;
+       }
+
+       /**
+        * Bump user_touched if it didn't change since this object was loaded
+        *
+        * On success, the mTouched field is updated.
+        * The user serialization cache is always cleared.
+        *
+        * @return bool Whether user_touched was actually updated
+        * @since 1.26
+        */
+       protected function checkAndSetTouched() {
+               $this->load();
+
+               if ( !$this->mId ) {
+                       return false; // anon
+               }
+
+               // Get a new user_touched that is higher than the old one
+               $oldTouched = $this->mTouched;
+               $newTouched = $this->newTouchedTimestamp();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update( 'user',
+                       array( 'user_touched' => $dbw->timestamp( $newTouched ) ),
+                       array(
+                               'user_id' => $this->mId,
+                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
+                       ),
+                       __METHOD__
+               );
+               $success = ( $dbw->affectedRows() > 0 );
+
+               if ( $success ) {
+                       $this->mTouched = $newTouched;
+                       $this->clearSharedCache();
+               } else {
+                       // Clears on failure too since that is desired if the cache is stale
+                       $this->clearSharedCache( 'refresh' );
+               }
+
+               return $success;
+       }
+
+       /**
+        * Clear various cached data stored in this object. The cache of the user table
+        * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
+        *
+        * @param bool|string $reloadFrom Reload user and user_groups table data from a
+        *   given source. May be "name", "id", "defaults", "session", or false for no reload.
+        */
+       public function clearInstanceCache( $reloadFrom = false ) {
+               $this->mNewtalk = -1;
+               $this->mDatePreference = null;
+               $this->mBlockedby = -1; # Unset
+               $this->mHash = false;
+               $this->mRights = null;
+               $this->mEffectiveGroups = null;
+               $this->mImplicitGroups = null;
+               $this->mGroups = null;
+               $this->mOptions = null;
+               $this->mOptionsLoaded = false;
+               $this->mEditCount = null;
+
+               if ( $reloadFrom ) {
+                       $this->mLoadedItems = array();
+                       $this->mFrom = $reloadFrom;
+               }
+       }
+
+       /**
+        * Combine the language default options with any site-specific options
+        * and add the default language variants.
+        *
+        * @return array Array of String options
+        */
+       public static function getDefaultOptions() {
+               global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
+
+               static $defOpt = null;
+               if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
+                       // Disabling this for the unit tests, as they rely on being able to change $wgContLang
+                       // mid-request and see that change reflected in the return value of this function.
+                       // Which is insane and would never happen during normal MW operation
+                       return $defOpt;
+               }
+
+               $defOpt = $wgDefaultUserOptions;
+               // Default language setting
+               $defOpt['language'] = $wgContLang->getCode();
+               foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
+                       $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
+               }
+               foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
+                       $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
+               }
+               $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
+
+               Hooks::run( 'UserGetDefaultOptions', array( &$defOpt ) );
+
+               return $defOpt;
+       }
+
+       /**
+        * Get a given default option value.
+        *
+        * @param string $opt Name of option to retrieve
+        * @return string Default option value
+        */
+       public static function getDefaultOption( $opt ) {
+               $defOpts = self::getDefaultOptions();
+               if ( isset( $defOpts[$opt] ) ) {
+                       return $defOpts[$opt];
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Get blocking information
+        * @param bool $bFromSlave Whether to check the slave database first.
+        *   To improve performance, non-critical checks are done against slaves.
+        *   Check when actually saving should be done against master.
+        */
+       private function getBlockedStatus( $bFromSlave = true ) {
+               global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
+
+               if ( -1 != $this->mBlockedby ) {
+                       return;
+               }
+
+               wfDebug( __METHOD__ . ": checking...\n" );
+
+               // Initialize data...
+               // Otherwise something ends up stomping on $this->mBlockedby when
+               // things get lazy-loaded later, causing false positive block hits
+               // due to -1 !== 0. Probably session-related... Nothing should be
+               // overwriting mBlockedby, surely?
+               $this->load();
+
+               # We only need to worry about passing the IP address to the Block generator if the
+               # user is not immune to autoblocks/hardblocks, and they are the current user so we
+               # know which IP address they're actually coming from
+               if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->equals( $wgUser ) ) {
+                       $ip = $this->getRequest()->getIP();
+               } else {
+                       $ip = null;
+               }
+
+               // User/IP blocking
+               $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
+
+               // Proxy blocking
+               if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
+                       && !in_array( $ip, $wgProxyWhitelist )
+               ) {
+                       // Local list
+                       if ( self::isLocallyBlockedProxy( $ip ) ) {
+                               $block = new Block;
+                               $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
+                               $block->mReason = wfMessage( 'proxyblockreason' )->text();
+                               $block->setTarget( $ip );
+                       } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
+                               $block = new Block;
+                               $block->setBlocker( wfMessage( 'sorbs' )->text() );
+                               $block->mReason = wfMessage( 'sorbsreason' )->text();
+                               $block->setTarget( $ip );
+                       }
+               }
+
+               // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
+               if ( !$block instanceof Block
+                       && $wgApplyIpBlocksToXff
+                       && $ip !== null
+                       && !$this->isAllowed( 'proxyunbannable' )
+                       && !in_array( $ip, $wgProxyWhitelist )
+               ) {
+                       $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
+                       $xff = array_map( 'trim', explode( ',', $xff ) );
+                       $xff = array_diff( $xff, array( $ip ) );
+                       $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
+                       $block = Block::chooseBlock( $xffblocks, $xff );
+                       if ( $block instanceof Block ) {
+                               # Mangle the reason to alert the user that the block
+                               # originated from matching the X-Forwarded-For header.
+                               $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
+                       }
+               }
+
+               if ( $block instanceof Block ) {
+                       wfDebug( __METHOD__ . ": Found block.\n" );
+                       $this->mBlock = $block;
+                       $this->mBlockedby = $block->getByName();
+                       $this->mBlockreason = $block->mReason;
+                       $this->mHideName = $block->mHideName;
+                       $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
+               } else {
+                       $this->mBlockedby = '';
+                       $this->mHideName = 0;
+                       $this->mAllowUsertalk = false;
+               }
+
+               // Extensions
+               Hooks::run( 'GetBlockedStatus', array( &$this ) );
+
+       }
+
+       /**
+        * Whether the given IP is in a DNS blacklist.
+        *
+        * @param string $ip IP to check
+        * @param bool $checkWhitelist Whether to check the whitelist first
+        * @return bool True if blacklisted.
+        */
+       public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
+               global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
+
+               if ( !$wgEnableDnsBlacklist ) {
+                       return false;
+               }
+
+               if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
+                       return false;
+               }
+
+               return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
+       }
+
+       /**
+        * Whether the given IP is in a given DNS blacklist.
+        *
+        * @param string $ip IP to check
+        * @param string|array $bases Array of Strings: URL of the DNS blacklist
+        * @return bool True if blacklisted.
+        */
+       public function inDnsBlacklist( $ip, $bases ) {
+
+               $found = false;
+               // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
+               if ( IP::isIPv4( $ip ) ) {
+                       // Reverse IP, bug 21255
+                       $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
+
+                       foreach ( (array)$bases as $base ) {
+                               // Make hostname
+                               // If we have an access key, use that too (ProjectHoneypot, etc.)
+                               $basename = $base;
+                               if ( is_array( $base ) ) {
+                                       if ( count( $base ) >= 2 ) {
+                                               // Access key is 1, base URL is 0
+                                               $host = "{$base[1]}.$ipReversed.{$base[0]}";
+                                       } else {
+                                               $host = "$ipReversed.{$base[0]}";
+                                       }
+                                       $basename = $base[0];
+                               } else {
+                                       $host = "$ipReversed.$base";
+                               }
+
+                               // Send query
+                               $ipList = gethostbynamel( $host );
+
+                               if ( $ipList ) {
+                                       wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
+                                       $found = true;
+                                       break;
+                               } else {
+                                       wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
+                               }
+                       }
+               }
+
+               return $found;
+       }
+
+       /**
+        * Check if an IP address is in the local proxy list
+        *
+        * @param string $ip
+        *
+        * @return bool
+        */
+       public static function isLocallyBlockedProxy( $ip ) {
+               global $wgProxyList;
+
+               if ( !$wgProxyList ) {
+                       return false;
+               }
+
+               if ( !is_array( $wgProxyList ) ) {
+                       // Load from the specified file
+                       $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
+               }
+
+               if ( !is_array( $wgProxyList ) ) {
+                       $ret = false;
+               } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
+                       $ret = true;
+               } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
+                       // Old-style flipped proxy list
+                       $ret = true;
+               } else {
+                       $ret = false;
+               }
+               return $ret;
+       }
+
+       /**
+        * Is this user subject to rate limiting?
+        *
+        * @return bool True if rate limited
+        */
+       public function isPingLimitable() {
+               global $wgRateLimitsExcludedIPs;
+               if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
+                       // No other good way currently to disable rate limits
+                       // for specific IPs. :P
+                       // But this is a crappy hack and should die.
+                       return false;
+               }
+               return !$this->isAllowed( 'noratelimit' );
+       }
+
+       /**
+        * Primitive rate limits: enforce maximum actions per time period
+        * to put a brake on flooding.
+        *
+        * The method generates both a generic profiling point and a per action one
+        * (suffix being "-$action".
+        *
+        * @note When using a shared cache like memcached, IP-address
+        * last-hit counters will be shared across wikis.
+        *
+        * @param string $action Action to enforce; 'edit' if unspecified
+        * @param int $incrBy Positive amount to increment counter by [defaults to 1]
+        * @return bool True if a rate limiter was tripped
+        */
+       public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
+               // Call the 'PingLimiter' hook
+               $result = false;
+               if ( !Hooks::run( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
+                       return $result;
+               }
+
+               global $wgRateLimits;
+               if ( !isset( $wgRateLimits[$action] ) ) {
+                       return false;
+               }
+
+               // Some groups shouldn't trigger the ping limiter, ever
+               if ( !$this->isPingLimitable() ) {
+                       return false;
+               }
+
+               $limits = $wgRateLimits[$action];
+               $keys = array();
+               $id = $this->getId();
+               $userLimit = false;
+
+               if ( isset( $limits['anon'] ) && $id == 0 ) {
+                       $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
+               }
+
+               if ( isset( $limits['user'] ) && $id != 0 ) {
+                       $userLimit = $limits['user'];
+               }
+               if ( $this->isNewbie() ) {
+                       if ( isset( $limits['newbie'] ) && $id != 0 ) {
+                               $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
+                       }
+                       if ( isset( $limits['ip'] ) ) {
+                               $ip = $this->getRequest()->getIP();
+                               $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
+                       }
+                       if ( isset( $limits['subnet'] ) ) {
+                               $ip = $this->getRequest()->getIP();
+                               $matches = array();
+                               $subnet = false;
+                               if ( IP::isIPv6( $ip ) ) {
+                                       $parts = IP::parseRange( "$ip/64" );
+                                       $subnet = $parts[0];
+                               } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
+                                       // IPv4
+                                       $subnet = $matches[1];
+                               }
+                               if ( $subnet !== false ) {
+                                       $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
+                               }
+                       }
+               }
+               // Check for group-specific permissions
+               // If more than one group applies, use the group with the highest limit
+               foreach ( $this->getGroups() as $group ) {
+                       if ( isset( $limits[$group] ) ) {
+                               if ( $userLimit === false
+                                       || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
+                               ) {
+                                       $userLimit = $limits[$group];
+                               }
+                       }
+               }
+               // Set the user limit key
+               if ( $userLimit !== false ) {
+                       list( $max, $period ) = $userLimit;
+                       wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
+                       $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
+               }
+
+               $cache = ObjectCache::getLocalClusterInstance();
+
+               $triggered = false;
+               foreach ( $keys as $key => $limit ) {
+                       list( $max, $period ) = $limit;
+                       $summary = "(limit $max in {$period}s)";
+                       $count = $cache->get( $key );
+                       // Already pinged?
+                       if ( $count ) {
+                               if ( $count >= $max ) {
+                                       wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
+                                               "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
+                                       $triggered = true;
+                               } else {
+                                       wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
+                               }
+                       } else {
+                               wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
+                               if ( $incrBy > 0 ) {
+                                       $cache->add( $key, 0, intval( $period ) ); // first ping
+                               }
+                       }
+                       if ( $incrBy > 0 ) {
+                               $cache->incr( $key, $incrBy );
+                       }
+               }
+
+               return $triggered;
+       }
+
+       /**
+        * Check if user is blocked
+        *
+        * @param bool $bFromSlave Whether to check the slave database instead of
+        *   the master. Hacked from false due to horrible probs on site.
+        * @return bool True if blocked, false otherwise
+        */
+       public function isBlocked( $bFromSlave = true ) {
+               return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
+       }
+
+       /**
+        * Get the block affecting the user, or null if the user is not blocked
+        *
+        * @param bool $bFromSlave Whether to check the slave database instead of the master
+        * @return Block|null
+        */
+       public function getBlock( $bFromSlave = true ) {
+               $this->getBlockedStatus( $bFromSlave );
+               return $this->mBlock instanceof Block ? $this->mBlock : null;
+       }
+
+       /**
+        * Check if user is blocked from editing a particular article
+        *
+        * @param Title $title Title to check
+        * @param bool $bFromSlave Whether to check the slave database instead of the master
+        * @return bool
+        */
+       public function isBlockedFrom( $title, $bFromSlave = false ) {
+               global $wgBlockAllowsUTEdit;
+
+               $blocked = $this->isBlocked( $bFromSlave );
+               $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
+               // If a user's name is suppressed, they cannot make edits anywhere
+               if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
+                       && $title->getNamespace() == NS_USER_TALK ) {
+                       $blocked = false;
+                       wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
+               }
+
+               Hooks::run( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
+
+               return $blocked;
+       }
+
+       /**
+        * If user is blocked, return the name of the user who placed the block
+        * @return string Name of blocker
+        */
+       public function blockedBy() {
+               $this->getBlockedStatus();
+               return $this->mBlockedby;
+       }
+
+       /**
+        * If user is blocked, return the specified reason for the block
+        * @return string Blocking reason
+        */
+       public function blockedFor() {
+               $this->getBlockedStatus();
+               return $this->mBlockreason;
+       }
+
+       /**
+        * If user is blocked, return the ID for the block
+        * @return int Block ID
+        */
+       public function getBlockId() {
+               $this->getBlockedStatus();
+               return ( $this->mBlock ? $this->mBlock->getId() : false );
+       }
+
+       /**
+        * Check if user is blocked on all wikis.
+        * Do not use for actual edit permission checks!
+        * This is intended for quick UI checks.
+        *
+        * @param string $ip IP address, uses current client if none given
+        * @return bool True if blocked, false otherwise
+        */
+       public function isBlockedGlobally( $ip = '' ) {
+               if ( $this->mBlockedGlobally !== null ) {
+                       return $this->mBlockedGlobally;
+               }
+               // User is already an IP?
+               if ( IP::isIPAddress( $this->getName() ) ) {
+                       $ip = $this->getName();
+               } elseif ( !$ip ) {
+                       $ip = $this->getRequest()->getIP();
+               }
+               $blocked = false;
+               Hooks::run( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
+               $this->mBlockedGlobally = (bool)$blocked;
+               return $this->mBlockedGlobally;
+       }
+
+       /**
+        * Check if user account is locked
+        *
+        * @return bool True if locked, false otherwise
+        */
+       public function isLocked() {
+               if ( $this->mLocked !== null ) {
+                       return $this->mLocked;
+               }
+               global $wgAuth;
+               $authUser = $wgAuth->getUserInstance( $this );
+               $this->mLocked = (bool)$authUser->isLocked();
+               Hooks::run( 'UserIsLocked', array( $this, &$this->mLocked ) );
+               return $this->mLocked;
+       }
+
+       /**
+        * Check if user account is hidden
+        *
+        * @return bool True if hidden, false otherwise
+        */
+       public function isHidden() {
+               if ( $this->mHideName !== null ) {
+                       return $this->mHideName;
+               }
+               $this->getBlockedStatus();
+               if ( !$this->mHideName ) {
+                       global $wgAuth;
+                       $authUser = $wgAuth->getUserInstance( $this );
+                       $this->mHideName = (bool)$authUser->isHidden();
+                       Hooks::run( 'UserIsHidden', array( $this, &$this->mHideName ) );
+               }
+               return $this->mHideName;
+       }
+
+       /**
+        * Get the user's ID.
+        * @return int The user's ID; 0 if the user is anonymous or nonexistent
+        */
+       public function getId() {
+               if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
+                       // Special case, we know the user is anonymous
+                       return 0;
+               } elseif ( !$this->isItemLoaded( 'id' ) ) {
+                       // Don't load if this was initialized from an ID
+                       $this->load();
+               }
+               return $this->mId;
+       }
+
+       /**
+        * Set the user and reload all fields according to a given ID
+        * @param int $v User ID to reload
+        */
+       public function setId( $v ) {
+               $this->mId = $v;
+               $this->clearInstanceCache( 'id' );
+       }
+
+       /**
+        * Get the user name, or the IP of an anonymous user
+        * @return string User's name or IP address
+        */
+       public function getName() {
+               if ( $this->isItemLoaded( 'name', 'only' ) ) {
+                       // Special case optimisation
+                       return $this->mName;
+               } else {
+                       $this->load();
+                       if ( $this->mName === false ) {
+                               // Clean up IPs
+                               $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
+                       }
+                       return $this->mName;
+               }
+       }
+
+       /**
+        * Set the user name.
+        *
+        * This does not reload fields from the database according to the given
+        * name. Rather, it is used to create a temporary "nonexistent user" for
+        * later addition to the database. It can also be used to set the IP
+        * address for an anonymous user to something other than the current
+        * remote IP.
+        *
+        * @note User::newFromName() has roughly the same function, when the named user
+        * does not exist.
+        * @param string $str New user name to set
+        */
+       public function setName( $str ) {
+               $this->load();
+               $this->mName = $str;
+       }
+
+       /**
+        * Get the user's name escaped by underscores.
+        * @return string Username escaped by underscores.
+        */
+       public function getTitleKey() {
+               return str_replace( ' ', '_', $this->getName() );
+       }
+
+       /**
+        * Check if the user has new messages.
+        * @return bool True if the user has new messages
+        */
+       public function getNewtalk() {
+               $this->load();
+
+               // Load the newtalk status if it is unloaded (mNewtalk=-1)
+               if ( $this->mNewtalk === -1 ) {
+                       $this->mNewtalk = false; # reset talk page status
+
+                       // Check memcached separately for anons, who have no
+                       // entire User object stored in there.
+                       if ( !$this->mId ) {
+                               global $wgDisableAnonTalk;
+                               if ( $wgDisableAnonTalk ) {
+                                       // Anon newtalk disabled by configuration.
+                                       $this->mNewtalk = false;
+                               } else {
+                                       $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
+                               }
+                       } else {
+                               $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
+                       }
+               }
+
+               return (bool)$this->mNewtalk;
+       }
+
+       /**
+        * Return the data needed to construct links for new talk page message
+        * alerts. If there are new messages, this will return an associative array
+        * with the following data:
+        *     wiki: The database name of the wiki
+        *     link: Root-relative link to the user's talk page
+        *     rev: The last talk page revision that the user has seen or null. This
+        *         is useful for building diff links.
+        * If there are no new messages, it returns an empty array.
+        * @note This function was designed to accomodate multiple talk pages, but
+        * currently only returns a single link and revision.
+        * @return array
+        */
+       public function getNewMessageLinks() {
+               $talks = array();
+               if ( !Hooks::run( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
+                       return $talks;
+               } elseif ( !$this->getNewtalk() ) {
+                       return array();
+               }
+               $utp = $this->getTalkPage();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Get the "last viewed rev" timestamp from the oldest message notification
+               $timestamp = $dbr->selectField( 'user_newtalk',
+                       'MIN(user_last_timestamp)',
+                       $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
+                       __METHOD__ );
+               $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
+               return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
+       }
+
+       /**
+        * Get the revision ID for the last talk page revision viewed by the talk
+        * page owner.
+        * @return int|null Revision ID or null
+        */
+       public function getNewMessageRevisionId() {
+               $newMessageRevisionId = null;
+               $newMessageLinks = $this->getNewMessageLinks();
+               if ( $newMessageLinks ) {
+                       // Note: getNewMessageLinks() never returns more than a single link
+                       // and it is always for the same wiki, but we double-check here in
+                       // case that changes some time in the future.
+                       if ( count( $newMessageLinks ) === 1
+                               && $newMessageLinks[0]['wiki'] === wfWikiID()
+                               && $newMessageLinks[0]['rev']
+                       ) {
+                               /** @var Revision $newMessageRevision */
+                               $newMessageRevision = $newMessageLinks[0]['rev'];
+                               $newMessageRevisionId = $newMessageRevision->getId();
+                       }
+               }
+               return $newMessageRevisionId;
+       }
+
+       /**
+        * Internal uncached check for new messages
+        *
+        * @see getNewtalk()
+        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+        * @param string|int $id User's IP address for anonymous users, User ID otherwise
+        * @return bool True if the user has new messages
+        */
+       protected function checkNewtalk( $field, $id ) {
+               $dbr = wfGetDB( DB_SLAVE );
+
+               $ok = $dbr->selectField( 'user_newtalk', $field, array( $field => $id ), __METHOD__ );
+
+               return $ok !== false;
+       }
+
+       /**
+        * Add or update the new messages flag
+        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+        * @param string|int $id User's IP address for anonymous users, User ID otherwise
+        * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null.
+        * @return bool True if successful, false otherwise
+        */
+       protected function updateNewtalk( $field, $id, $curRev = null ) {
+               // Get timestamp of the talk page revision prior to the current one
+               $prevRev = $curRev ? $curRev->getPrevious() : false;
+               $ts = $prevRev ? $prevRev->getTimestamp() : null;
+               // Mark the user as having new messages since this revision
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->insert( 'user_newtalk',
+                       array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
+                       __METHOD__,
+                       'IGNORE' );
+               if ( $dbw->affectedRows() ) {
+                       wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
+                       return true;
+               } else {
+                       wfDebug( __METHOD__ . " already set ($field, $id)\n" );
+                       return false;
+               }
+       }
+
+       /**
+        * Clear the new messages flag for the given user
+        * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
+        * @param string|int $id User's IP address for anonymous users, User ID otherwise
+        * @return bool True if successful, false otherwise
+        */
+       protected function deleteNewtalk( $field, $id ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete( 'user_newtalk',
+                       array( $field => $id ),
+                       __METHOD__ );
+               if ( $dbw->affectedRows() ) {
+                       wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
+                       return true;
+               } else {
+                       wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
+                       return false;
+               }
+       }
+
+       /**
+        * Update the 'You have new messages!' status.
+        * @param bool $val Whether the user has new messages
+        * @param Revision $curRev New, as yet unseen revision of the user talk
+        *   page. Ignored if null or !$val.
+        */
+       public function setNewtalk( $val, $curRev = null ) {
+               if ( wfReadOnly() ) {
+                       return;
+               }
+
+               $this->load();
+               $this->mNewtalk = $val;
+
+               if ( $this->isAnon() ) {
+                       $field = 'user_ip';
+                       $id = $this->getName();
+               } else {
+                       $field = 'user_id';
+                       $id = $this->getId();
+               }
+
+               if ( $val ) {
+                       $changed = $this->updateNewtalk( $field, $id, $curRev );
+               } else {
+                       $changed = $this->deleteNewtalk( $field, $id );
+               }
+
+               if ( $changed ) {
+                       $this->invalidateCache();
+               }
+       }
+
+       /**
+        * Generate a current or new-future timestamp to be stored in the
+        * user_touched field when we update things.
+        * @return string Timestamp in TS_MW format
+        */
+       private function newTouchedTimestamp() {
+               global $wgClockSkewFudge;
+
+               $time = wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
+               if ( $this->mTouched && $time <= $this->mTouched ) {
+                       $time = wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
+               }
+
+               return $time;
+       }
+
+       /**
+        * Clear user data from memcached
+        *
+        * Use after applying updates to the database; caller's
+        * responsibility to update user_touched if appropriate.
+        *
+        * Called implicitly from invalidateCache() and saveSettings().
+        *
+        * @param string $mode Use 'refresh' to clear now; otherwise before DB commit
+        */
+       public function clearSharedCache( $mode = 'changed' ) {
+               if ( !$this->getId() ) {
+                       return;
+               }
+
+               $cache = ObjectCache::getMainWANInstance();
+               $key = $this->getCacheKey( $cache );
+               if ( $mode === 'refresh' ) {
+                       $cache->delete( $key, 1 );
+               } else {
+                       wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) {
+                               $cache->delete( $key );
+                       } );
+               }
+       }
+
+       /**
+        * Immediately touch the user data cache for this account
+        *
+        * Calls touch() and removes account data from memcached
+        */
+       public function invalidateCache() {
+               $this->touch();
+               $this->clearSharedCache();
+       }
+
+       /**
+        * Update the "touched" timestamp for the user
+        *
+        * This is useful on various login/logout events when making sure that
+        * a browser or proxy that has multiple tenants does not suffer cache
+        * pollution where the new user sees the old users content. The value
+        * of getTouched() is checked when determining 304 vs 200 responses.
+        * Unlike invalidateCache(), this preserves the User object cache and
+        * avoids database writes.
+        *
+        * @since 1.25
+        */
+       public function touch() {
+               $id = $this->getId();
+               if ( $id ) {
+                       $key = wfMemcKey( 'user-quicktouched', 'id', $id );
+                       ObjectCache::getMainWANInstance()->touchCheckKey( $key );
+                       $this->mQuickTouched = null;
+               }
+       }
+
+       /**
+        * Validate the cache for this account.
+        * @param string $timestamp A timestamp in TS_MW format
+        * @return bool
+        */
+       public function validateCache( $timestamp ) {
+               return ( $timestamp >= $this->getTouched() );
+       }
+
+       /**
+        * Get the user touched timestamp
+        *
+        * Use this value only to validate caches via inequalities
+        * such as in the case of HTTP If-Modified-Since response logic
+        *
+        * @return string TS_MW Timestamp
+        */
+       public function getTouched() {
+               $this->load();
+
+               if ( $this->mId ) {
+                       if ( $this->mQuickTouched === null ) {
+                               $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
+                               $cache = ObjectCache::getMainWANInstance();
+
+                               $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
+                       }
+
+                       return max( $this->mTouched, $this->mQuickTouched );
+               }
+
+               return $this->mTouched;
+       }
+
+       /**
+        * Get the user_touched timestamp field (time of last DB updates)
+        * @return string TS_MW Timestamp
+        * @since 1.26
+        */
+       public function getDBTouched() {
+               $this->load();
+
+               return $this->mTouched;
+       }
+
+       /**
+        * @deprecated Removed in 1.27.
+        * @return Password
+        * @since 1.24
+        */
+       public function getPassword() {
+               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
+       }
+
+       /**
+        * @deprecated Removed in 1.27.
+        * @return Password
+        * @since 1.24
+        */
+       public function getTemporaryPassword() {
+               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
+       }
+
+       /**
+        * Set the password and reset the random token.
+        * Calls through to authentication plugin if necessary;
+        * will have no effect if the auth plugin refuses to
+        * pass the change through or if the legal password
+        * checks fail.
+        *
+        * As a special case, setting the password to null
+        * wipes it, so the account cannot be logged in until
+        * a new password is set, for instance via e-mail.
+        *
+        * @deprecated since 1.27. AuthManager is coming.
+        * @param string $str New password to set
+        * @throws PasswordError On failure
+        * @return bool
+        */
+       public function setPassword( $str ) {
+               global $wgAuth;
+
+               if ( $str !== null ) {
+                       if ( !$wgAuth->allowPasswordChange() ) {
+                               throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
+                       }
+
+                       $status = $this->checkPasswordValidity( $str );
+                       if ( !$status->isGood() ) {
+                               throw new PasswordError( $status->getMessage()->text() );
+                       }
+               }
+
+               if ( !$wgAuth->setPassword( $this, $str ) ) {
+                       throw new PasswordError( wfMessage( 'externaldberror' )->text() );
+               }
+
+               $this->setToken();
+               $this->setOption( 'watchlisttoken', false );
+               $this->setPasswordInternal( $str );
+
+               return true;
+       }
+
+       /**
+        * Set the password and reset the random token unconditionally.
+        *
+        * @deprecated since 1.27. AuthManager is coming.
+        * @param string|null $str New password to set or null to set an invalid
+        *  password hash meaning that the user will not be able to log in
+        *  through the web interface.
+        */
+       public function setInternalPassword( $str ) {
+               global $wgAuth;
+
+               if ( $wgAuth->allowSetLocalPassword() ) {
+                       $this->setToken();
+                       $this->setOption( 'watchlisttoken', false );
+                       $this->setPasswordInternal( $str );
+               }
+       }
+
+       /**
+        * Actually set the password and such
+        * @since 1.27 cannot set a password for a user not in the database
+        * @param string|null $str New password to set or null to set an invalid
+        *  password hash meaning that the user will not be able to log in
+        *  through the web interface.
+        */
+       private function setPasswordInternal( $str ) {
+               $id = self::idFromName( $this->getName(), self::READ_LATEST );
+               if ( $id == 0 ) {
+                       throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+               }
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       array(
+                               'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
+                               'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
+                               'user_newpass_time' => $dbw->timestampOrNull( null ),
+                       ),
+                       array(
+                               'user_id' => $id,
+                       ),
+                       __METHOD__
+               );
+       }
+
+       /**
+        * Get the user's current token.
+        * @param bool $forceCreation Force the generation of a new token if the
+        *   user doesn't have one (default=true for backwards compatibility).
+        * @return string Token
+        */
+       public function getToken( $forceCreation = true ) {
+               $this->load();
+               if ( !$this->mToken && $forceCreation ) {
+                       $this->setToken();
+               }
+               return $this->mToken;
+       }
+
+       /**
+        * Set the random token (used for persistent authentication)
+        * Called from loadDefaults() among other places.
+        *
+        * @param string|bool $token If specified, set the token to this value
+        */
+       public function setToken( $token = false ) {
+               $this->load();
+               if ( !$token ) {
+                       $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
+               } else {
+                       $this->mToken = $token;
+               }
+       }
+
+       /**
+        * Set the password for a password reminder or new account email
+        *
+        * @deprecated since 1.27, AuthManager is coming
+        * @param string $str New password to set or null to set an invalid
+        *  password hash meaning that the user will not be able to use it
+        * @param bool $throttle If true, reset the throttle timestamp to the present
+        */
+       public function setNewpassword( $str, $throttle = true ) {
+               $id = $this->getId();
+               if ( $id == 0 ) {
+                       throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $update = array(
+                       'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
+               );
+
+               if ( $str === null ) {
+                       $update['user_newpass_time'] = null;
+               } elseif ( $throttle ) {
+                       $update['user_newpass_time'] = $dbw->timestamp();
+               }
+
+               $dbw->update( 'user', $update, array( 'user_id' => $id ), __METHOD__ );
+       }
+
+       /**
+        * Has password reminder email been sent within the last
+        * $wgPasswordReminderResendTime hours?
+        * @return bool
+        */
+       public function isPasswordReminderThrottled() {
+               global $wgPasswordReminderResendTime;
+
+               if ( !$wgPasswordReminderResendTime ) {
+                       return false;
+               }
+
+               $this->load();
+
+               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+               $newpassTime = $db->selectField(
+                       'user',
+                       'user_newpass_time',
+                       array( 'user_id' => $this->getId() ),
+                       __METHOD__
+               );
+
+               if ( $newpassTime === null ) {
+                       return false;
+               }
+               $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
+               return time() < $expiry;
+       }
+
+       /**
+        * Get the user's e-mail address
+        * @return string User's email address
+        */
+       public function getEmail() {
+               $this->load();
+               Hooks::run( 'UserGetEmail', array( $this, &$this->mEmail ) );
+               return $this->mEmail;
+       }
+
+       /**
+        * Get the timestamp of the user's e-mail authentication
+        * @return string TS_MW timestamp
+        */
+       public function getEmailAuthenticationTimestamp() {
+               $this->load();
+               Hooks::run( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
+               return $this->mEmailAuthenticated;
+       }
+
+       /**
+        * Set the user's e-mail address
+        * @param string $str New e-mail address
+        */
+       public function setEmail( $str ) {
+               $this->load();
+               if ( $str == $this->mEmail ) {
+                       return;
+               }
+               $this->invalidateEmail();
+               $this->mEmail = $str;
+               Hooks::run( 'UserSetEmail', array( $this, &$this->mEmail ) );
+       }
+
+       /**
+        * Set the user's e-mail address and a confirmation mail if needed.
+        *
+        * @since 1.20
+        * @param string $str New e-mail address
+        * @return Status
+        */
+       public function setEmailWithConfirmation( $str ) {
+               global $wgEnableEmail, $wgEmailAuthentication;
+
+               if ( !$wgEnableEmail ) {
+                       return Status::newFatal( 'emaildisabled' );
+               }
+
+               $oldaddr = $this->getEmail();
+               if ( $str === $oldaddr ) {
+                       return Status::newGood( true );
+               }
+
+               $this->setEmail( $str );
+
+               if ( $str !== '' && $wgEmailAuthentication ) {
+                       // Send a confirmation request to the new address if needed
+                       $type = $oldaddr != '' ? 'changed' : 'set';
+                       $result = $this->sendConfirmationMail( $type );
+                       if ( $result->isGood() ) {
+                               // Say to the caller that a confirmation mail has been sent
+                               $result->value = 'eauth';
+                       }
+               } else {
+                       $result = Status::newGood( true );
+               }
+
+               return $result;
+       }
+
+       /**
+        * Get the user's real name
+        * @return string User's real name
+        */
+       public function getRealName() {
+               if ( !$this->isItemLoaded( 'realname' ) ) {
+                       $this->load();
+               }
+
+               return $this->mRealName;
+       }
+
+       /**
+        * Set the user's real name
+        * @param string $str New real name
+        */
+       public function setRealName( $str ) {
+               $this->load();
+               $this->mRealName = $str;
+       }
+
+       /**
+        * Get the user's current setting for a given option.
+        *
+        * @param string $oname The option to check
+        * @param string $defaultOverride A default value returned if the option does not exist
+        * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
+        * @return string User's current value for the option
+        * @see getBoolOption()
+        * @see getIntOption()
+        */
+       public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
+               global $wgHiddenPrefs;
+               $this->loadOptions();
+
+               # We want 'disabled' preferences to always behave as the default value for
+               # users, even if they have set the option explicitly in their settings (ie they
+               # set it, and then it was disabled removing their ability to change it).  But
+               # we don't want to erase the preferences in the database in case the preference
+               # is re-enabled again.  So don't touch $mOptions, just override the returned value
+               if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
+                       return self::getDefaultOption( $oname );
+               }
+
+               if ( array_key_exists( $oname, $this->mOptions ) ) {
+                       return $this->mOptions[$oname];
+               } else {
+                       return $defaultOverride;
+               }
+       }
+
+       /**
+        * Get all user's options
+        *
+        * @param int $flags Bitwise combination of:
+        *   User::GETOPTIONS_EXCLUDE_DEFAULTS  Exclude user options that are set
+        *                                      to the default value. (Since 1.25)
+        * @return array
+        */
+       public function getOptions( $flags = 0 ) {
+               global $wgHiddenPrefs;
+               $this->loadOptions();
+               $options = $this->mOptions;
+
+               # We want 'disabled' preferences to always behave as the default value for
+               # users, even if they have set the option explicitly in their settings (ie they
+               # set it, and then it was disabled removing their ability to change it).  But
+               # we don't want to erase the preferences in the database in case the preference
+               # is re-enabled again.  So don't touch $mOptions, just override the returned value
+               foreach ( $wgHiddenPrefs as $pref ) {
+                       $default = self::getDefaultOption( $pref );
+                       if ( $default !== null ) {
+                               $options[$pref] = $default;
+                       }
+               }
+
+               if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
+                       $options = array_diff_assoc( $options, self::getDefaultOptions() );
+               }
+
+               return $options;
+       }
+
+       /**
+        * Get the user's current setting for a given option, as a boolean value.
+        *
+        * @param string $oname The option to check
+        * @return bool User's current value for the option
+        * @see getOption()
+        */
+       public function getBoolOption( $oname ) {
+               return (bool)$this->getOption( $oname );
+       }
+
+       /**
+        * Get the user's current setting for a given option, as an integer value.
+        *
+        * @param string $oname The option to check
+        * @param int $defaultOverride A default value returned if the option does not exist
+        * @return int User's current value for the option
+        * @see getOption()
+        */
+       public function getIntOption( $oname, $defaultOverride = 0 ) {
+               $val = $this->getOption( $oname );
+               if ( $val == '' ) {
+                       $val = $defaultOverride;
+               }
+               return intval( $val );
+       }
+
+       /**
+        * Set the given option for a user.
+        *
+        * You need to call saveSettings() to actually write to the database.
+        *
+        * @param string $oname The option to set
+        * @param mixed $val New value to set
+        */
+       public function setOption( $oname, $val ) {
+               $this->loadOptions();
+
+               // Explicitly NULL values should refer to defaults
+               if ( is_null( $val ) ) {
+                       $val = self::getDefaultOption( $oname );
+               }
+
+               $this->mOptions[$oname] = $val;
+       }
+
+       /**
+        * Get a token stored in the preferences (like the watchlist one),
+        * resetting it if it's empty (and saving changes).
+        *
+        * @param string $oname The option name to retrieve the token from
+        * @return string|bool User's current value for the option, or false if this option is disabled.
+        * @see resetTokenFromOption()
+        * @see getOption()
+        * @deprecated 1.26 Applications should use the OAuth extension
+        */
+       public function getTokenFromOption( $oname ) {
+               global $wgHiddenPrefs;
+
+               $id = $this->getId();
+               if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
+                       return false;
+               }
+
+               $token = $this->getOption( $oname );
+               if ( !$token ) {
+                       // Default to a value based on the user token to avoid space
+                       // wasted on storing tokens for all users. When this option
+                       // is set manually by the user, only then is it stored.
+                       $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
+               }
+
+               return $token;
+       }
+
+       /**
+        * Reset a token stored in the preferences (like the watchlist one).
+        * *Does not* save user's preferences (similarly to setOption()).
+        *
+        * @param string $oname The option name to reset the token in
+        * @return string|bool New token value, or false if this option is disabled.
+        * @see getTokenFromOption()
+        * @see setOption()
+        */
+       public function resetTokenFromOption( $oname ) {
+               global $wgHiddenPrefs;
+               if ( in_array( $oname, $wgHiddenPrefs ) ) {
+                       return false;
+               }
+
+               $token = MWCryptRand::generateHex( 40 );
+               $this->setOption( $oname, $token );
+               return $token;
+       }
+
+       /**
+        * Return a list of the types of user options currently returned by
+        * User::getOptionKinds().
+        *
+        * Currently, the option kinds are:
+        * - 'registered' - preferences which are registered in core MediaWiki or
+        *                  by extensions using the UserGetDefaultOptions hook.
+        * - 'registered-multiselect' - as above, using the 'multiselect' type.
+        * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
+        * - 'userjs' - preferences with names starting with 'userjs-', intended to
+        *              be used by user scripts.
+        * - 'special' - "preferences" that are not accessible via User::getOptions
+        *               or User::setOptions.
+        * - 'unused' - preferences about which MediaWiki doesn't know anything.
+        *              These are usually legacy options, removed in newer versions.
+        *
+        * The API (and possibly others) use this function to determine the possible
+        * option types for validation purposes, so make sure to update this when a
+        * new option kind is added.
+        *
+        * @see User::getOptionKinds
+        * @return array Option kinds
+        */
+       public static function listOptionKinds() {
+               return array(
+                       'registered',
+                       'registered-multiselect',
+                       'registered-checkmatrix',
+                       'userjs',
+                       'special',
+                       'unused'
+               );
+       }
+
+       /**
+        * Return an associative array mapping preferences keys to the kind of a preference they're
+        * used for. Different kinds are handled differently when setting or reading preferences.
+        *
+        * See User::listOptionKinds for the list of valid option types that can be provided.
+        *
+        * @see User::listOptionKinds
+        * @param IContextSource $context
+        * @param array $options Assoc. array with options keys to check as keys.
+        *   Defaults to $this->mOptions.
+        * @return array The key => kind mapping data
+        */
+       public function getOptionKinds( IContextSource $context, $options = null ) {
+               $this->loadOptions();
+               if ( $options === null ) {
+                       $options = $this->mOptions;
+               }
+
+               $prefs = Preferences::getPreferences( $this, $context );
+               $mapping = array();
+
+               // Pull out the "special" options, so they don't get converted as
+               // multiselect or checkmatrix.
+               $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
+               foreach ( $specialOptions as $name => $value ) {
+                       unset( $prefs[$name] );
+               }
+
+               // Multiselect and checkmatrix options are stored in the database with
+               // one key per option, each having a boolean value. Extract those keys.
+               $multiselectOptions = array();
+               foreach ( $prefs as $name => $info ) {
+                       if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
+                                       ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
+                               $opts = HTMLFormField::flattenOptions( $info['options'] );
+                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+
+                               foreach ( $opts as $value ) {
+                                       $multiselectOptions["$prefix$value"] = true;
+                               }
+
+                               unset( $prefs[$name] );
+                       }
+               }
+               $checkmatrixOptions = array();
+               foreach ( $prefs as $name => $info ) {
+                       if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
+                                       ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
+                               $columns = HTMLFormField::flattenOptions( $info['columns'] );
+                               $rows = HTMLFormField::flattenOptions( $info['rows'] );
+                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+
+                               foreach ( $columns as $column ) {
+                                       foreach ( $rows as $row ) {
+                                               $checkmatrixOptions["$prefix$column-$row"] = true;
+                                       }
+                               }
+
+                               unset( $prefs[$name] );
+                       }
+               }
+
+               // $value is ignored
+               foreach ( $options as $key => $value ) {
+                       if ( isset( $prefs[$key] ) ) {
+                               $mapping[$key] = 'registered';
+                       } elseif ( isset( $multiselectOptions[$key] ) ) {
+                               $mapping[$key] = 'registered-multiselect';
+                       } elseif ( isset( $checkmatrixOptions[$key] ) ) {
+                               $mapping[$key] = 'registered-checkmatrix';
+                       } elseif ( isset( $specialOptions[$key] ) ) {
+                               $mapping[$key] = 'special';
+                       } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
+                               $mapping[$key] = 'userjs';
+                       } else {
+                               $mapping[$key] = 'unused';
+                       }
+               }
+
+               return $mapping;
+       }
+
+       /**
+        * Reset certain (or all) options to the site defaults
+        *
+        * The optional parameter determines which kinds of preferences will be reset.
+        * Supported values are everything that can be reported by getOptionKinds()
+        * and 'all', which forces a reset of *all* preferences and overrides everything else.
+        *
+        * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
+        *  array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
+        *  for backwards-compatibility.
+        * @param IContextSource|null $context Context source used when $resetKinds
+        *  does not contain 'all', passed to getOptionKinds().
+        *  Defaults to RequestContext::getMain() when null.
+        */
+       public function resetOptions(
+               $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
+               IContextSource $context = null
+       ) {
+               $this->load();
+               $defaultOptions = self::getDefaultOptions();
+
+               if ( !is_array( $resetKinds ) ) {
+                       $resetKinds = array( $resetKinds );
+               }
+
+               if ( in_array( 'all', $resetKinds ) ) {
+                       $newOptions = $defaultOptions;
+               } else {
+                       if ( $context === null ) {
+                               $context = RequestContext::getMain();
+                       }
+
+                       $optionKinds = $this->getOptionKinds( $context );
+                       $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
+                       $newOptions = array();
+
+                       // Use default values for the options that should be deleted, and
+                       // copy old values for the ones that shouldn't.
+                       foreach ( $this->mOptions as $key => $value ) {
+                               if ( in_array( $optionKinds[$key], $resetKinds ) ) {
+                                       if ( array_key_exists( $key, $defaultOptions ) ) {
+                                               $newOptions[$key] = $defaultOptions[$key];
+                                       }
+                               } else {
+                                       $newOptions[$key] = $value;
+                               }
+                       }
+               }
+
+               Hooks::run( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
+
+               $this->mOptions = $newOptions;
+               $this->mOptionsLoaded = true;
+       }
+
+       /**
+        * Get the user's preferred date format.
+        * @return string User's preferred date format
+        */
+       public function getDatePreference() {
+               // Important migration for old data rows
+               if ( is_null( $this->mDatePreference ) ) {
+                       global $wgLang;
+                       $value = $this->getOption( 'date' );
+                       $map = $wgLang->getDatePreferenceMigrationMap();
+                       if ( isset( $map[$value] ) ) {
+                               $value = $map[$value];
+                       }
+                       $this->mDatePreference = $value;
+               }
+               return $this->mDatePreference;
+       }
+
+       /**
+        * Determine based on the wiki configuration and the user's options,
+        * whether this user must be over HTTPS no matter what.
+        *
+        * @return bool
+        */
+       public function requiresHTTPS() {
+               global $wgSecureLogin;
+               if ( !$wgSecureLogin ) {
+                       return false;
+               } else {
+                       $https = $this->getBoolOption( 'prefershttps' );
+                       Hooks::run( 'UserRequiresHTTPS', array( $this, &$https ) );
+                       if ( $https ) {
+                               $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
+                       }
+                       return $https;
+               }
+       }
+
+       /**
+        * Get the user preferred stub threshold
+        *
+        * @return int
+        */
+       public function getStubThreshold() {
+               global $wgMaxArticleSize; # Maximum article size, in Kb
+               $threshold = $this->getIntOption( 'stubthreshold' );
+               if ( $threshold > $wgMaxArticleSize * 1024 ) {
+                       // If they have set an impossible value, disable the preference
+                       // so we can use the parser cache again.
+                       $threshold = 0;
+               }
+               return $threshold;
+       }
+
+       /**
+        * Get the permissions this user has.
+        * @return array Array of String permission names
+        */
+       public function getRights() {
+               if ( is_null( $this->mRights ) ) {
+                       $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+                       Hooks::run( 'UserGetRights', array( $this, &$this->mRights ) );
+                       // Force reindexation of rights when a hook has unset one of them
+                       $this->mRights = array_values( array_unique( $this->mRights ) );
+               }
+               return $this->mRights;
+       }
+
+       /**
+        * Get the list of explicit group memberships this user has.
+        * The implicit * and user groups are not included.
+        * @return array Array of String internal group names
+        */
+       public function getGroups() {
+               $this->load();
+               $this->loadGroups();
+               return $this->mGroups;
+       }
+
+       /**
+        * Get the list of implicit group memberships this user has.
+        * This includes all explicit groups, plus 'user' if logged in,
+        * '*' for all accounts, and autopromoted groups
+        * @param bool $recache Whether to avoid the cache
+        * @return array Array of String internal group names
+        */
+       public function getEffectiveGroups( $recache = false ) {
+               if ( $recache || is_null( $this->mEffectiveGroups ) ) {
+                       $this->mEffectiveGroups = array_unique( array_merge(
+                               $this->getGroups(), // explicit groups
+                               $this->getAutomaticGroups( $recache ) // implicit groups
+                       ) );
+                       // Hook for additional groups
+                       Hooks::run( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
+                       // Force reindexation of groups when a hook has unset one of them
+                       $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
+               }
+               return $this->mEffectiveGroups;
+       }
+
+       /**
+        * Get the list of implicit group memberships this user has.
+        * This includes 'user' if logged in, '*' for all accounts,
+        * and autopromoted groups
+        * @param bool $recache Whether to avoid the cache
+        * @return array Array of String internal group names
+        */
+       public function getAutomaticGroups( $recache = false ) {
+               if ( $recache || is_null( $this->mImplicitGroups ) ) {
+                       $this->mImplicitGroups = array( '*' );
+                       if ( $this->getId() ) {
+                               $this->mImplicitGroups[] = 'user';
+
+                               $this->mImplicitGroups = array_unique( array_merge(
+                                       $this->mImplicitGroups,
+                                       Autopromote::getAutopromoteGroups( $this )
+                               ) );
+                       }
+                       if ( $recache ) {
+                               // Assure data consistency with rights/groups,
+                               // as getEffectiveGroups() depends on this function
+                               $this->mEffectiveGroups = null;
+                       }
+               }
+               return $this->mImplicitGroups;
+       }
+
+       /**
+        * Returns the groups the user has belonged to.
+        *
+        * The user may still belong to the returned groups. Compare with getGroups().
+        *
+        * The function will not return groups the user had belonged to before MW 1.17
+        *
+        * @return array Names of the groups the user has belonged to.
+        */
+       public function getFormerGroups() {
+               $this->load();
+
+               if ( is_null( $this->mFormerGroups ) ) {
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
+                       $res = $db->select( 'user_former_groups',
+                               array( 'ufg_group' ),
+                               array( 'ufg_user' => $this->mId ),
+                               __METHOD__ );
+                       $this->mFormerGroups = array();
+                       foreach ( $res as $row ) {
+                               $this->mFormerGroups[] = $row->ufg_group;
+                       }
+               }
+
+               return $this->mFormerGroups;
+       }
+
+       /**
+        * Get the user's edit count.
+        * @return int|null Null for anonymous users
+        */
+       public function getEditCount() {
+               if ( !$this->getId() ) {
+                       return null;
+               }
+
+               if ( $this->mEditCount === null ) {
+                       /* Populate the count, if it has not been populated yet */
+                       $dbr = wfGetDB( DB_SLAVE );
+                       // check if the user_editcount field has been initialized
+                       $count = $dbr->selectField(
+                               'user', 'user_editcount',
+                               array( 'user_id' => $this->mId ),
+                               __METHOD__
+                       );
+
+                       if ( $count === null ) {
+                               // it has not been initialized. do so.
+                               $count = $this->initEditCount();
+                       }
+                       $this->mEditCount = $count;
+               }
+               return (int)$this->mEditCount;
+       }
+
+       /**
+        * Add the user to the given group.
+        * This takes immediate effect.
+        * @param string $group Name of the group to add
+        * @return bool
+        */
+       public function addGroup( $group ) {
+               $this->load();
+
+               if ( !Hooks::run( 'UserAddGroup', array( $this, &$group ) ) ) {
+                       return false;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               if ( $this->getId() ) {
+                       $dbw->insert( 'user_groups',
+                               array(
+                                       'ug_user' => $this->getID(),
+                                       'ug_group' => $group,
+                               ),
+                               __METHOD__,
+                               array( 'IGNORE' ) );
+               }
+
+               $this->loadGroups();
+               $this->mGroups[] = $group;
+               // In case loadGroups was not called before, we now have the right twice.
+               // Get rid of the duplicate.
+               $this->mGroups = array_unique( $this->mGroups );
+
+               // Refresh the groups caches, and clear the rights cache so it will be
+               // refreshed on the next call to $this->getRights().
+               $this->getEffectiveGroups( true );
+               $this->mRights = null;
+
+               $this->invalidateCache();
+
+               return true;
+       }
+
+       /**
+        * Remove the user from the given group.
+        * This takes immediate effect.
+        * @param string $group Name of the group to remove
+        * @return bool
+        */
+       public function removeGroup( $group ) {
+               $this->load();
+               if ( !Hooks::run( 'UserRemoveGroup', array( $this, &$group ) ) ) {
+                       return false;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete( 'user_groups',
+                       array(
+                               'ug_user' => $this->getID(),
+                               'ug_group' => $group,
+                       ), __METHOD__
+               );
+               // Remember that the user was in this group
+               $dbw->insert( 'user_former_groups',
+                       array(
+                               'ufg_user' => $this->getID(),
+                               'ufg_group' => $group,
+                       ),
+                       __METHOD__,
+                       array( 'IGNORE' )
+               );
+
+               $this->loadGroups();
+               $this->mGroups = array_diff( $this->mGroups, array( $group ) );
+
+               // Refresh the groups caches, and clear the rights cache so it will be
+               // refreshed on the next call to $this->getRights().
+               $this->getEffectiveGroups( true );
+               $this->mRights = null;
+
+               $this->invalidateCache();
+
+               return true;
+       }
+
+       /**
+        * Get whether the user is logged in
+        * @return bool
+        */
+       public function isLoggedIn() {
+               return $this->getID() != 0;
+       }
+
+       /**
+        * Get whether the user is anonymous
+        * @return bool
+        */
+       public function isAnon() {
+               return !$this->isLoggedIn();
+       }
+
+       /**
+        * Check if user is allowed to access a feature / make an action
+        *
+        * @param string ... Permissions to test
+        * @return bool True if user is allowed to perform *any* of the given actions
+        */
+       public function isAllowedAny() {
+               $permissions = func_get_args();
+               foreach ( $permissions as $permission ) {
+                       if ( $this->isAllowed( $permission ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        *
+        * @param string ... Permissions to test
+        * @return bool True if the user is allowed to perform *all* of the given actions
+        */
+       public function isAllowedAll() {
+               $permissions = func_get_args();
+               foreach ( $permissions as $permission ) {
+                       if ( !$this->isAllowed( $permission ) ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Internal mechanics of testing a permission
+        * @param string $action
+        * @return bool
+        */
+       public function isAllowed( $action = '' ) {
+               if ( $action === '' ) {
+                       return true; // In the spirit of DWIM
+               }
+               // Patrolling may not be enabled
+               if ( $action === 'patrol' || $action === 'autopatrol' ) {
+                       global $wgUseRCPatrol, $wgUseNPPatrol;
+                       if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
+                               return false;
+                       }
+               }
+               // Use strict parameter to avoid matching numeric 0 accidentally inserted
+               // by misconfiguration: 0 == 'foo'
+               return in_array( $action, $this->getRights(), true );
+       }
+
+       /**
+        * Check whether to enable recent changes patrol features for this user
+        * @return bool True or false
+        */
+       public function useRCPatrol() {
+               global $wgUseRCPatrol;
+               return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
+       }
+
+       /**
+        * Check whether to enable new pages patrol features for this user
+        * @return bool True or false
+        */
+       public function useNPPatrol() {
+               global $wgUseRCPatrol, $wgUseNPPatrol;
+               return (
+                       ( $wgUseRCPatrol || $wgUseNPPatrol )
+                               && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
+               );
+       }
+
+       /**
+        * Get the WebRequest object to use with this object
+        *
+        * @return WebRequest
+        */
+       public function getRequest() {
+               if ( $this->mRequest ) {
+                       return $this->mRequest;
+               } else {
+                       global $wgRequest;
+                       return $wgRequest;
+               }
+       }
+
+       /**
+        * Get the current skin, loading it if required
+        * @return Skin The current skin
+        * @todo FIXME: Need to check the old failback system [AV]
+        * @deprecated since 1.18 Use ->getSkin() in the most relevant outputting context you have
+        */
+       public function getSkin() {
+               wfDeprecated( __METHOD__, '1.18' );
+               return RequestContext::getMain()->getSkin();
+       }
+
+       /**
+        * Get a WatchedItem for this user and $title.
+        *
+        * @since 1.22 $checkRights parameter added
+        * @param Title $title
+        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+        * @return WatchedItem
+        */
+       public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+               $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
+
+               if ( isset( $this->mWatchedItems[$key] ) ) {
+                       return $this->mWatchedItems[$key];
+               }
+
+               if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
+                       $this->mWatchedItems = array();
+               }
+
+               $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
+               return $this->mWatchedItems[$key];
+       }
+
+       /**
+        * Check the watched status of an article.
+        * @since 1.22 $checkRights parameter added
+        * @param Title $title Title of the article to look at
+        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+        * @return bool
+        */
+       public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+               return $this->getWatchedItem( $title, $checkRights )->isWatched();
+       }
+
+       /**
+        * Watch an article.
+        * @since 1.22 $checkRights parameter added
+        * @param Title $title Title of the article to look at
+        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+        */
+       public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+               $this->getWatchedItem( $title, $checkRights )->addWatch();
+               $this->invalidateCache();
+       }
+
+       /**
+        * Stop watching an article.
+        * @since 1.22 $checkRights parameter added
+        * @param Title $title Title of the article to look at
+        * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+        *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+        */
+       public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
+               $this->getWatchedItem( $title, $checkRights )->removeWatch();
+               $this->invalidateCache();
+       }
+
+       /**
+        * Clear the user's notification timestamp for the given title.
+        * If e-notif e-mails are on, they will receive notification mails on
+        * the next change of the page if it's watched etc.
+        * @note If the user doesn't have 'editmywatchlist', this will do nothing.
+        * @param Title $title Title of the article to look at
+        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
+        */
+       public function clearNotification( &$title, $oldid = 0 ) {
+               global $wgUseEnotif, $wgShowUpdatedMarker;
+
+               // Do nothing if the database is locked to writes
+               if ( wfReadOnly() ) {
+                       return;
+               }
+
+               // Do nothing if not allowed to edit the watchlist
+               if ( !$this->isAllowed( 'editmywatchlist' ) ) {
+                       return;
+               }
+
+               // If we're working on user's talk page, we should update the talk page message indicator
+               if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
+                       if ( !Hooks::run( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
+                               return;
+                       }
+
+                       $that = $this;
+                       // Try to update the DB post-send and only if needed...
+                       DeferredUpdates::addCallableUpdate( function() use ( $that, $title, $oldid ) {
+                               if ( !$that->getNewtalk() ) {
+                                       return; // no notifications to clear
+                               }
+
+                               // Delete the last notifications (they stack up)
+                               $that->setNewtalk( false );
+
+                               // If there is a new, unseen, revision, use its timestamp
+                               $nextid = $oldid
+                                       ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
+                                       : null;
+                               if ( $nextid ) {
+                                       $that->setNewtalk( true, Revision::newFromId( $nextid ) );
+                               }
+                       } );
+               }
+
+               if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
+                       return;
+               }
+
+               if ( $this->isAnon() ) {
+                       // Nothing else to do...
+                       return;
+               }
+
+               // Only update the timestamp if the page is being watched.
+               // The query to find out if it is watched is cached both in memcached and per-invocation,
+               // and when it does have to be executed, it can be on a slave
+               // If this is the user's newtalk page, we always update the timestamp
+               $force = '';
+               if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
+                       $force = 'force';
+               }
+
+               $this->getWatchedItem( $title )->resetNotificationTimestamp(
+                       $force, $oldid, WatchedItem::DEFERRED
+               );
+       }
+
+       /**
+        * Resets all of the given user's page-change notification timestamps.
+        * If e-notif e-mails are on, they will receive notification mails on
+        * the next change of any watched page.
+        * @note If the user doesn't have 'editmywatchlist', this will do nothing.
+        */
+       public function clearAllNotifications() {
+               if ( wfReadOnly() ) {
+                       return;
+               }
+
+               // Do nothing if not allowed to edit the watchlist
+               if ( !$this->isAllowed( 'editmywatchlist' ) ) {
+                       return;
+               }
+
+               global $wgUseEnotif, $wgShowUpdatedMarker;
+               if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
+                       $this->setNewtalk( false );
+                       return;
+               }
+               $id = $this->getId();
+               if ( $id != 0 ) {
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->update( 'watchlist',
+                               array( /* SET */ 'wl_notificationtimestamp' => null ),
+                               array( /* WHERE */ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ),
+                               __METHOD__
+                       );
+                       // We also need to clear here the "you have new message" notification for the own user_talk page;
+                       // it's cleared one page view later in WikiPage::doViewUpdates().
+               }
+       }
+
+       /**
+        * Set a cookie on the user's client. Wrapper for
+        * WebResponse::setCookie
+        * @param string $name Name of the cookie to set
+        * @param string $value Value to set
+        * @param int $exp Expiration time, as a UNIX time value;
+        *                   if 0 or not specified, use the default $wgCookieExpiration
+        * @param bool $secure
+        *  true: Force setting the secure attribute when setting the cookie
+        *  false: Force NOT setting the secure attribute when setting the cookie
+        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
+        * @param array $params Array of options sent passed to WebResponse::setcookie()
+        * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
+        *        is passed.
+        */
+       protected function setCookie(
+               $name, $value, $exp = 0, $secure = null, $params = array(), $request = null
+       ) {
+               if ( $request === null ) {
+                       $request = $this->getRequest();
+               }
+               $params['secure'] = $secure;
+               $request->response()->setCookie( $name, $value, $exp, $params );
+       }
+
+       /**
+        * Clear a cookie on the user's client
+        * @param string $name Name of the cookie to clear
+        * @param bool $secure
+        *  true: Force setting the secure attribute when setting the cookie
+        *  false: Force NOT setting the secure attribute when setting the cookie
+        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
+        * @param array $params Array of options sent passed to WebResponse::setcookie()
+        */
+       protected function clearCookie( $name, $secure = null, $params = array() ) {
+               $this->setCookie( $name, '', time() - 86400, $secure, $params );
+       }
+
+       /**
+        * Set an extended login cookie on the user's client. The expiry of the cookie
+        * is controlled by the $wgExtendedLoginCookieExpiration configuration
+        * variable.
+        *
+        * @see User::setCookie
+        *
+        * @param string $name Name of the cookie to set
+        * @param string $value Value to set
+        * @param bool $secure
+        *  true: Force setting the secure attribute when setting the cookie
+        *  false: Force NOT setting the secure attribute when setting the cookie
+        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
+        */
+       protected function setExtendedLoginCookie( $name, $value, $secure ) {
+               global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
+
+               $exp = time();
+               $exp += $wgExtendedLoginCookieExpiration !== null
+                       ? $wgExtendedLoginCookieExpiration
+                       : $wgCookieExpiration;
+
+               $this->setCookie( $name, $value, $exp, $secure );
+       }
+
+       /**
+        * Set the default cookies for this session on the user's client.
+        *
+        * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
+        *        is passed.
+        * @param bool $secure Whether to force secure/insecure cookies or use default
+        * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
+        */
+       public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
+               global $wgExtendedLoginCookies;
+
+               if ( $request === null ) {
+                       $request = $this->getRequest();
+               }
+
+               $this->load();
+               if ( 0 == $this->mId ) {
+                       return;
+               }
+               if ( !$this->mToken ) {
+                       // When token is empty or NULL generate a new one and then save it to the database
+                       // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
+                       // Simply by setting every cell in the user_token column to NULL and letting them be
+                       // regenerated as users log back into the wiki.
+                       $this->setToken();
+                       if ( !wfReadOnly() ) {
+                               $this->saveSettings();
+                       }
+               }
+               $session = array(
+                       'wsUserID' => $this->mId,
+                       'wsToken' => $this->mToken,
+                       'wsUserName' => $this->getName()
+               );
+               $cookies = array(
+                       'UserID' => $this->mId,
+                       'UserName' => $this->getName(),
+               );
+               if ( $rememberMe ) {
+                       $cookies['Token'] = $this->mToken;
+               } else {
+                       $cookies['Token'] = false;
+               }
+
+               Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
+
+               foreach ( $session as $name => $value ) {
+                       $request->setSessionData( $name, $value );
+               }
+               foreach ( $cookies as $name => $value ) {
+                       if ( $value === false ) {
+                               $this->clearCookie( $name );
+                       } elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
+                               $this->setExtendedLoginCookie( $name, $value, $secure );
+                       } else {
+                               $this->setCookie( $name, $value, 0, $secure, array(), $request );
+                       }
+               }
+
+               /**
+                * If wpStickHTTPS was selected, also set an insecure cookie that
+                * will cause the site to redirect the user to HTTPS, if they access
+                * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
+                * as the one set by centralauth (bug 53538). Also set it to session, or
+                * standard time setting, based on if rememberme was set.
+                */
+               if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
+                       $this->setCookie(
+                               'forceHTTPS',
+                               'true',
+                               $rememberMe ? 0 : null,
+                               false,
+                               array( 'prefix' => '' ) // no prefix
+                       );
+               }
+       }
+
+       /**
+        * Log this user out.
+        */
+       public function logout() {
+               if ( Hooks::run( 'UserLogout', array( &$this ) ) ) {
+                       $this->doLogout();
+               }
+       }
+
+       /**
+        * Clear the user's cookies and session, and reset the instance cache.
+        * @see logout()
+        */
+       public function doLogout() {
+               $this->clearInstanceCache( 'defaults' );
+
+               $this->getRequest()->setSessionData( 'wsUserID', 0 );
+
+               $this->clearCookie( 'UserID' );
+               $this->clearCookie( 'Token' );
+               $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
+
+               // Remember when user logged out, to prevent seeing cached pages
+               $this->setCookie( 'LoggedOut', time(), time() + 86400 );
+       }
+
+       /**
+        * Save this user's settings into the database.
+        * @todo Only rarely do all these fields need to be set!
+        */
+       public function saveSettings() {
+               if ( wfReadOnly() ) {
+                       // @TODO: caller should deal with this instead!
+                       // This should really just be an exception.
+                       MWExceptionHandler::logException( new DBExpectedError(
+                               null,
+                               "Could not update user with ID '{$this->mId}'; DB is read-only."
+                       ) );
+                       return;
+               }
+
+               $this->load();
+               if ( 0 == $this->mId ) {
+                       return; // anon
+               }
+
+               // Get a new user_touched that is higher than the old one.
+               // This will be used for a CAS check as a last-resort safety
+               // check against race conditions and slave lag.
+               $oldTouched = $this->mTouched;
+               $newTouched = $this->newTouchedTimestamp();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update( 'user',
+                       array( /* SET */
+                               'user_name' => $this->mName,
+                               'user_real_name' => $this->mRealName,
+                               'user_email' => $this->mEmail,
+                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+                               'user_touched' => $dbw->timestamp( $newTouched ),
+                               'user_token' => strval( $this->mToken ),
+                               'user_email_token' => $this->mEmailToken,
+                               'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
+                       ), array( /* WHERE */
+                               'user_id' => $this->mId,
+                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
+                       ), __METHOD__
+               );
+
+               if ( !$dbw->affectedRows() ) {
+                       // Maybe the problem was a missed cache update; clear it to be safe
+                       $this->clearSharedCache( 'refresh' );
+                       // User was changed in the meantime or loaded with stale data
+                       $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'slave';
+                       throw new MWException(
+                               "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
+                               " the version of the user to be saved is older than the current version."
+                       );
+               }
+
+               $this->mTouched = $newTouched;
+               $this->saveOptions();
+
+               Hooks::run( 'UserSaveSettings', array( $this ) );
+               $this->clearSharedCache();
+               $this->getUserPage()->invalidateCache();
+       }
+
+       /**
+        * If only this user's username is known, and it exists, return the user ID.
+        *
+        * @param int $flags Bitfield of User:READ_* constants; useful for existence checks
+        * @return int
+        */
+       public function idForName( $flags = 0 ) {
+               $s = trim( $this->getName() );
+               if ( $s === '' ) {
+                       return 0;
+               }
+
+               $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+
+               $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
+                       ? array( 'LOCK IN SHARE MODE' )
+                       : array();
+
+               $id = $db->selectField( 'user',
+                       'user_id', array( 'user_name' => $s ), __METHOD__, $options );
+
+               return (int)$id;
+       }
+
+       /**
+        * Add a user to the database, return the user object
+        *
+        * @param string $name Username to add
+        * @param array $params Array of Strings Non-default parameters to save to
+        *   the database as user_* fields:
+        *   - email: The user's email address.
+        *   - email_authenticated: The email authentication timestamp.
+        *   - real_name: The user's real name.
+        *   - options: An associative array of non-default options.
+        *   - token: Random authentication token. Do not set.
+        *   - registration: Registration timestamp. Do not set.
+        *
+        * @return User|null User object, or null if the username already exists.
+        */
+       public static function createNew( $name, $params = array() ) {
+               foreach ( array( 'password', 'newpassword', 'newpass_time', 'password_expires' ) as $field ) {
+                       if ( isset( $params[$field] ) ) {
+                               wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
+                               unset( $params[$field] );
+                       }
+               }
+
+               $user = new User;
+               $user->load();
+               $user->setToken(); // init token
+               if ( isset( $params['options'] ) ) {
+                       $user->mOptions = $params['options'] + (array)$user->mOptions;
+                       unset( $params['options'] );
+               }
+               $dbw = wfGetDB( DB_MASTER );
+               $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
+
+               $noPass = PasswordFactory::newInvalidPassword()->toString();
+
+               $fields = array(
+                       'user_id' => $seqVal,
+                       'user_name' => $name,
+                       'user_password' => $noPass,
+                       'user_newpassword' => $noPass,
+                       'user_email' => $user->mEmail,
+                       'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
+                       'user_real_name' => $user->mRealName,
+                       'user_token' => strval( $user->mToken ),
+                       'user_registration' => $dbw->timestamp( $user->mRegistration ),
+                       'user_editcount' => 0,
+                       'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
+               );
+               foreach ( $params as $name => $value ) {
+                       $fields["user_$name"] = $value;
+               }
+               $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
+               if ( $dbw->affectedRows() ) {
+                       $newUser = User::newFromId( $dbw->insertId() );
+               } else {
+                       $newUser = null;
+               }
+               return $newUser;
+       }
+
+       /**
+        * Add this existing user object to the database. If the user already
+        * exists, a fatal status object is returned, and the user object is
+        * initialised with the data from the database.
+        *
+        * Previously, this function generated a DB error due to a key conflict
+        * if the user already existed. Many extension callers use this function
+        * in code along the lines of:
+        *
+        *   $user = User::newFromName( $name );
+        *   if ( !$user->isLoggedIn() ) {
+        *       $user->addToDatabase();
+        *   }
+        *   // do something with $user...
+        *
+        * However, this was vulnerable to a race condition (bug 16020). By
+        * initialising the user object if the user exists, we aim to support this
+        * calling sequence as far as possible.
+        *
+        * Note that if the user exists, this function will acquire a write lock,
+        * so it is still advisable to make the call conditional on isLoggedIn(),
+        * and to commit the transaction after calling.
+        *
+        * @throws MWException
+        * @return Status
+        */
+       public function addToDatabase() {
+               $this->load();
+               if ( !$this->mToken ) {
+                       $this->setToken(); // init token
+               }
+
+               $this->mTouched = $this->newTouchedTimestamp();
+
+               $noPass = PasswordFactory::newInvalidPassword()->toString();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $inWrite = $dbw->writesOrCallbacksPending();
+               $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
+               $dbw->insert( 'user',
+                       array(
+                               'user_id' => $seqVal,
+                               'user_name' => $this->mName,
+                               'user_password' => $noPass,
+                               'user_newpassword' => $noPass,
+                               'user_email' => $this->mEmail,
+                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+                               'user_real_name' => $this->mRealName,
+                               'user_token' => strval( $this->mToken ),
+                               'user_registration' => $dbw->timestamp( $this->mRegistration ),
+                               'user_editcount' => 0,
+                               'user_touched' => $dbw->timestamp( $this->mTouched ),
+                       ), __METHOD__,
+                       array( 'IGNORE' )
+               );
+               if ( !$dbw->affectedRows() ) {
+                       // The queries below cannot happen in the same REPEATABLE-READ snapshot.
+                       // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
+                       if ( $inWrite ) {
+                               // Can't commit due to pending writes that may need atomicity.
+                               // This may cause some lock contention unlike the case below.
+                               $options = array( 'LOCK IN SHARE MODE' );
+                               $flags = self::READ_LOCKING;
+                       } else {
+                               // Often, this case happens early in views before any writes when
+                               // using CentralAuth. It's should be OK to commit and break the snapshot.
+                               $dbw->commit( __METHOD__, 'flush' );
+                               $options = array();
+                               $flags = self::READ_LATEST;
+                       }
+                       $this->mId = $dbw->selectField( 'user', 'user_id',
+                               array( 'user_name' => $this->mName ), __METHOD__, $options );
+                       $loaded = false;
+                       if ( $this->mId ) {
+                               if ( $this->loadFromDatabase( $flags ) ) {
+                                       $loaded = true;
+                               }
+                       }
+                       if ( !$loaded ) {
+                               throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
+                                       "to insert user '{$this->mName}' row, but it was not present in select!" );
+                       }
+                       return Status::newFatal( 'userexists' );
+               }
+               $this->mId = $dbw->insertId();
+               self::$idCacheByName[$this->mName] = $this->mId;
+
+               // Clear instance cache other than user table data, which is already accurate
+               $this->clearInstanceCache();
+
+               $this->saveOptions();
+               return Status::newGood();
+       }
+
+       /**
+        * If this user is logged-in and blocked,
+        * block any IP address they've successfully logged in from.
+        * @return bool A block was spread
+        */
+       public function spreadAnyEditBlock() {
+               if ( $this->isLoggedIn() && $this->isBlocked() ) {
+                       return $this->spreadBlock();
+               }
+               return false;
+       }
+
+       /**
+        * If this (non-anonymous) user is blocked,
+        * block the IP address they've successfully logged in from.
+        * @return bool A block was spread
+        */
+       protected function spreadBlock() {
+               wfDebug( __METHOD__ . "()\n" );
+               $this->load();
+               if ( $this->mId == 0 ) {
+                       return false;
+               }
+
+               $userblock = Block::newFromTarget( $this->getName() );
+               if ( !$userblock ) {
+                       return false;
+               }
+
+               return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
+       }
+
+       /**
+        * Get whether the user is explicitly blocked from account creation.
+        * @return bool|Block
+        */
+       public function isBlockedFromCreateAccount() {
+               $this->getBlockedStatus();
+               if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
+                       return $this->mBlock;
+               }
+
+               # bug 13611: if the IP address the user is trying to create an account from is
+               # blocked with createaccount disabled, prevent new account creation there even
+               # when the user is logged in
+               if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
+                       $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
+               }
+               return $this->mBlockedFromCreateAccount instanceof Block
+                       && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
+                       ? $this->mBlockedFromCreateAccount
+                       : false;
+       }
+
+       /**
+        * Get whether the user is blocked from using Special:Emailuser.
+        * @return bool
+        */
+       public function isBlockedFromEmailuser() {
+               $this->getBlockedStatus();
+               return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
+       }
+
+       /**
+        * Get whether the user is allowed to create an account.
+        * @return bool
+        */
+       public function isAllowedToCreateAccount() {
+               return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
+       }
+
+       /**
+        * Get this user's personal page title.
+        *
+        * @return Title User's personal page title
+        */
+       public function getUserPage() {
+               return Title::makeTitle( NS_USER, $this->getName() );
+       }
+
+       /**
+        * Get this user's talk page title.
+        *
+        * @return Title User's talk page title
+        */
+       public function getTalkPage() {
+               $title = $this->getUserPage();
+               return $title->getTalkPage();
+       }
+
+       /**
+        * Determine whether the user is a newbie. Newbies are either
+        * anonymous IPs, or the most recently created accounts.
+        * @return bool
+        */
+       public function isNewbie() {
+               return !$this->isAllowed( 'autoconfirmed' );
+       }
+
+       /**
+        * Check to see if the given clear-text password is one of the accepted passwords
+        * @deprecated since 1.27. AuthManager is coming.
+        * @param string $password User password
+        * @return bool True if the given password is correct, otherwise False
+        */
+       public function checkPassword( $password ) {
+               global $wgAuth, $wgLegacyEncoding;
+
+               $this->load();
+
+               // Some passwords will give a fatal Status, which means there is
+               // some sort of technical or security reason for this password to
+               // be completely invalid and should never be checked (e.g., T64685)
+               if ( !$this->checkPasswordValidity( $password )->isOK() ) {
+                       return false;
+               }
+
+               // Certain authentication plugins do NOT want to save
+               // domain passwords in a mysql database, so we should
+               // check this (in case $wgAuth->strict() is false).
+               if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
+                       return true;
+               } elseif ( $wgAuth->strict() ) {
+                       // Auth plugin doesn't allow local authentication
+                       return false;
+               } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
+                       // Auth plugin doesn't allow local authentication for this user name
+                       return false;
+               }
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+
+               try {
+                       $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
+                               'user', 'user_password', array( 'user_id' => $this->getId() ), __METHOD__
+                       ) );
+               } catch ( PasswordError $e ) {
+                       wfDebug( 'Invalid password hash found in database.' );
+                       $mPassword = PasswordFactory::newInvalidPassword();
+               }
+
+               if ( !$mPassword->equals( $password ) ) {
+                       if ( $wgLegacyEncoding ) {
+                               // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+                               // Check for this with iconv
+                               $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
+                               if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
+                                       return false;
+                               }
+                       } else {
+                               return false;
+                       }
+               }
+
+               if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
+                       $this->setPasswordInternal( $password );
+               }
+
+               return true;
+       }
+
+       /**
+        * Check if the given clear-text password matches the temporary password
+        * sent by e-mail for password reset operations.
+        *
+        * @deprecated since 1.27. AuthManager is coming.
+        * @param string $plaintext
+        * @return bool True if matches, false otherwise
+        */
+       public function checkTemporaryPassword( $plaintext ) {
+               global $wgNewPasswordExpiry;
+
+               $this->load();
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_SLAVE );
+
+               $row = $db->selectRow(
+                       'user',
+                       array( 'user_newpassword', 'user_newpass_time' ),
+                       array( 'user_id' => $this->getId() ),
+                       __METHOD__
+               );
+               try {
+                       $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+               } catch ( PasswordError $e ) {
+                       wfDebug( 'Invalid password hash found in database.' );
+                       $newPassword = PasswordFactory::newInvalidPassword();
+               }
+
+               if ( $newPassword->equals( $plaintext ) ) {
+                       if ( is_null( $row->user_newpass_time ) ) {
+                               return true;
+                       }
+                       $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
+                       return ( time() < $expiry );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Alias for getEditToken.
+        * @deprecated since 1.19, use getEditToken instead.
+        *
+        * @param string|array $salt Array of Strings Optional function-specific data for hashing
+        * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
+        * @return string The new edit token
+        */
+       public function editToken( $salt = '', $request = null ) {
+               wfDeprecated( __METHOD__, '1.19' );
+               return $this->getEditToken( $salt, $request );
+       }
+
+       /**
+        * Internal implementation for self::getEditToken() and
+        * self::matchEditToken().
+        *
+        * @param string|array $salt
+        * @param WebRequest $request
+        * @param string|int $timestamp
+        * @return string
+        */
+       private function getEditTokenAtTimestamp( $salt, $request, $timestamp ) {
+               if ( $this->isAnon() ) {
+                       return self::EDIT_TOKEN_SUFFIX;
+               } else {
+                       $token = $request->getSessionData( 'wsEditToken' );
+                       if ( $token === null ) {
+                               $token = MWCryptRand::generateHex( 32 );
+                               $request->setSessionData( 'wsEditToken', $token );
+                       }
+                       if ( is_array( $salt ) ) {
+                               $salt = implode( '|', $salt );
+                       }
+                       return hash_hmac( 'md5', $timestamp . $salt, $token, false ) .
+                               dechex( $timestamp ) .
+                               self::EDIT_TOKEN_SUFFIX;
+               }
+       }
+
+       /**
+        * Initialize (if necessary) and return a session token value
+        * which can be used in edit forms to show that the user's
+        * login credentials aren't being hijacked with a foreign form
+        * submission.
+        *
+        * @since 1.19
+        *
+        * @param string|array $salt Array of Strings Optional function-specific data for hashing
+        * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
+        * @return string The new edit token
+        */
+       public function getEditToken( $salt = '', $request = null ) {
+               return $this->getEditTokenAtTimestamp(
+                       $salt, $request ?: $this->getRequest(), wfTimestamp()
+               );
+       }
+
+       /**
+        * Generate a looking random token for various uses.
+        *
+        * @return string The new random token
+        * @deprecated since 1.20: Use MWCryptRand for secure purposes or
+        *   wfRandomString for pseudo-randomness.
+        */
+       public static function generateToken() {
+               return MWCryptRand::generateHex( 32 );
+       }
+
+       /**
+        * Get the embedded timestamp from a token.
+        * @param string $val Input token
+        * @return int|null
+        */
+       public static function getEditTokenTimestamp( $val ) {
+               $suffixLen = strlen( self::EDIT_TOKEN_SUFFIX );
+               if ( strlen( $val ) <= 32 + $suffixLen ) {
+                       return null;
+               }
+
+               return hexdec( substr( $val, 32, -$suffixLen ) );
+       }
+
+       /**
+        * Check given value against the token value stored in the session.
+        * A match should confirm that the form was submitted from the
+        * user's own login session, not a form submission from a third-party
+        * site.
+        *
+        * @param string $val Input value to compare
+        * @param string $salt Optional function-specific data for hashing
+        * @param WebRequest|null $request Object to use or null to use $wgRequest
+        * @param int $maxage Fail tokens older than this, in seconds
+        * @return bool Whether the token matches
+        */
+       public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
+               if ( $this->isAnon() ) {
+                       return $val === self::EDIT_TOKEN_SUFFIX;
+               }
+
+               $timestamp = self::getEditTokenTimestamp( $val );
+               if ( $timestamp === null ) {
+                       return false;
+               }
+               if ( $maxage !== null && $timestamp < wfTimestamp() - $maxage ) {
+                       // Expired token
+                       return false;
+               }
+
+               $sessionToken = $this->getEditTokenAtTimestamp(
+                       $salt, $request ?: $this->getRequest(), $timestamp
+               );
+
+               if ( $val != $sessionToken ) {
+                       wfDebug( "User::matchEditToken: broken session data\n" );
+               }
+
+               return hash_equals( $sessionToken, $val );
+       }
+
+       /**
+        * Check given value against the token value stored in the session,
+        * ignoring the suffix.
+        *
+        * @param string $val Input value to compare
+        * @param string $salt Optional function-specific data for hashing
+        * @param WebRequest|null $request Object to use or null to use $wgRequest
+        * @param int $maxage Fail tokens older than this, in seconds
+        * @return bool Whether the token matches
+        */
+       public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
+               $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . self::EDIT_TOKEN_SUFFIX;
+               return $this->matchEditToken( $val, $salt, $request, $maxage );
+       }
+
+       /**
+        * Generate a new e-mail confirmation token and send a confirmation/invalidation
+        * mail to the user's given address.
+        *
+        * @param string $type Message to send, either "created", "changed" or "set"
+        * @return Status
+        */
+       public function sendConfirmationMail( $type = 'created' ) {
+               global $wgLang;
+               $expiration = null; // gets passed-by-ref and defined in next line.
+               $token = $this->confirmationToken( $expiration );
+               $url = $this->confirmationTokenUrl( $token );
+               $invalidateURL = $this->invalidationTokenUrl( $token );
+               $this->saveSettings();
+
+               if ( $type == 'created' || $type === false ) {
+                       $message = 'confirmemail_body';
+               } elseif ( $type === true ) {
+                       $message = 'confirmemail_body_changed';
+               } else {
+                       // Messages: confirmemail_body_changed, confirmemail_body_set
+                       $message = 'confirmemail_body_' . $type;
+               }
+
+               return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
+                       wfMessage( $message,
+                               $this->getRequest()->getIP(),
+                               $this->getName(),
+                               $url,
+                               $wgLang->timeanddate( $expiration, false ),
+                               $invalidateURL,
+                               $wgLang->date( $expiration, false ),
+                               $wgLang->time( $expiration, false ) )->text() );
+       }
+
+       /**
+        * Send an e-mail to this user's account. Does not check for
+        * confirmed status or validity.
+        *
+        * @param string $subject Message subject
+        * @param string $body Message body
+        * @param User|null $from Optional sending user; if unspecified, default
+        *   $wgPasswordSender will be used.
+        * @param string $replyto Reply-To address
+        * @return Status
+        */
+       public function sendMail( $subject, $body, $from = null, $replyto = null ) {
+               global $wgPasswordSender;
+
+               if ( $from instanceof User ) {
+                       $sender = MailAddress::newFromUser( $from );
+               } else {
+                       $sender = new MailAddress( $wgPasswordSender,
+                               wfMessage( 'emailsender' )->inContentLanguage()->text() );
+               }
+               $to = MailAddress::newFromUser( $this );
+
+               return UserMailer::send( $to, $sender, $subject, $body, array(
+                       'replyTo' => $replyto,
+               ) );
+       }
+
+       /**
+        * Generate, store, and return a new e-mail confirmation code.
+        * A hash (unsalted, since it's used as a key) is stored.
+        *
+        * @note Call saveSettings() after calling this function to commit
+        * this change to the database.
+        *
+        * @param string &$expiration Accepts the expiration time
+        * @return string New token
+        */
+       protected function confirmationToken( &$expiration ) {
+               global $wgUserEmailConfirmationTokenExpiry;
+               $now = time();
+               $expires = $now + $wgUserEmailConfirmationTokenExpiry;
+               $expiration = wfTimestamp( TS_MW, $expires );
+               $this->load();
+               $token = MWCryptRand::generateHex( 32 );
+               $hash = md5( $token );
+               $this->mEmailToken = $hash;
+               $this->mEmailTokenExpires = $expiration;
+               return $token;
+       }
+
+       /**
+        * Return a URL the user can use to confirm their email address.
+        * @param string $token Accepts the email confirmation token
+        * @return string New token URL
+        */
+       protected function confirmationTokenUrl( $token ) {
+               return $this->getTokenUrl( 'ConfirmEmail', $token );
+       }
+
+       /**
+        * Return a URL the user can use to invalidate their email address.
+        * @param string $token Accepts the email confirmation token
+        * @return string New token URL
+        */
+       protected function invalidationTokenUrl( $token ) {
+               return $this->getTokenUrl( 'InvalidateEmail', $token );
+       }
+
+       /**
+        * Internal function to format the e-mail validation/invalidation URLs.
+        * This uses a quickie hack to use the
+        * hardcoded English names of the Special: pages, for ASCII safety.
+        *
+        * @note Since these URLs get dropped directly into emails, using the
+        * short English names avoids insanely long URL-encoded links, which
+        * also sometimes can get corrupted in some browsers/mailers
+        * (bug 6957 with Gmail and Internet Explorer).
+        *
+        * @param string $page Special page
+        * @param string $token Token
+        * @return string Formatted URL
+        */
+       protected function getTokenUrl( $page, $token ) {
+               // Hack to bypass localization of 'Special:'
+               $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
+               return $title->getCanonicalURL();
+       }
+
+       /**
+        * Mark the e-mail address confirmed.
+        *
+        * @note Call saveSettings() after calling this function to commit the change.
+        *
+        * @return bool
+        */
+       public function confirmEmail() {
+               // Check if it's already confirmed, so we don't touch the database
+               // and fire the ConfirmEmailComplete hook on redundant confirmations.
+               if ( !$this->isEmailConfirmed() ) {
+                       $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
+                       Hooks::run( 'ConfirmEmailComplete', array( $this ) );
+               }
+               return true;
+       }
+
+       /**
+        * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
+        * address if it was already confirmed.
+        *
+        * @note Call saveSettings() after calling this function to commit the change.
+        * @return bool Returns true
+        */
+       public function invalidateEmail() {
+               $this->load();
+               $this->mEmailToken = null;
+               $this->mEmailTokenExpires = null;
+               $this->setEmailAuthenticationTimestamp( null );
+               $this->mEmail = '';
+               Hooks::run( 'InvalidateEmailComplete', array( $this ) );
+               return true;
+       }
+
+       /**
+        * Set the e-mail authentication timestamp.
+        * @param string $timestamp TS_MW timestamp
+        */
+       public function setEmailAuthenticationTimestamp( $timestamp ) {
+               $this->load();
+               $this->mEmailAuthenticated = $timestamp;
+               Hooks::run( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
+       }
+
+       /**
+        * Is this user allowed to send e-mails within limits of current
+        * site configuration?
+        * @return bool
+        */
+       public function canSendEmail() {
+               global $wgEnableEmail, $wgEnableUserEmail;
+               if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
+                       return false;
+               }
+               $canSend = $this->isEmailConfirmed();
+               Hooks::run( 'UserCanSendEmail', array( &$this, &$canSend ) );
+               return $canSend;
+       }
+
+       /**
+        * Is this user allowed to receive e-mails within limits of current
+        * site configuration?
+        * @return bool
+        */
+       public function canReceiveEmail() {
+               return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
+       }
+
+       /**
+        * Is this user's e-mail address valid-looking and confirmed within
+        * limits of the current site configuration?
+        *
+        * @note If $wgEmailAuthentication is on, this may require the user to have
+        * confirmed their address by returning a code or using a password
+        * sent to the address from the wiki.
+        *
+        * @return bool
+        */
+       public function isEmailConfirmed() {
+               global $wgEmailAuthentication;
+               $this->load();
+               $confirmed = true;
+               if ( Hooks::run( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
+                       if ( $this->isAnon() ) {
+                               return false;
+                       }
+                       if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
+                               return false;
+                       }
+                       if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
+                               return false;
+                       }
+                       return true;
+               } else {
+                       return $confirmed;
+               }
+       }
+
+       /**
+        * Check whether there is an outstanding request for e-mail confirmation.
+        * @return bool
+        */
+       public function isEmailConfirmationPending() {
+               global $wgEmailAuthentication;
+               return $wgEmailAuthentication &&
+                       !$this->isEmailConfirmed() &&
+                       $this->mEmailToken &&
+                       $this->mEmailTokenExpires > wfTimestamp();
+       }
+
+       /**
+        * Get the timestamp of account creation.
+        *
+        * @return string|bool|null Timestamp of account creation, false for
+        *  non-existent/anonymous user accounts, or null if existing account
+        *  but information is not in database.
+        */
+       public function getRegistration() {
+               if ( $this->isAnon() ) {
+                       return false;
+               }
+               $this->load();
+               return $this->mRegistration;
+       }
+
+       /**
+        * Get the timestamp of the first edit
+        *
+        * @return string|bool Timestamp of first edit, or false for
+        *  non-existent/anonymous user accounts.
+        */
+       public function getFirstEditTimestamp() {
+               if ( $this->getId() == 0 ) {
+                       return false; // anons
+               }
+               $dbr = wfGetDB( DB_SLAVE );
+               $time = $dbr->selectField( 'revision', 'rev_timestamp',
+                       array( 'rev_user' => $this->getId() ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_timestamp ASC' )
+               );
+               if ( !$time ) {
+                       return false; // no edits
+               }
+               return wfTimestamp( TS_MW, $time );
+       }
+
+       /**
+        * Get the permissions associated with a given list of groups
+        *
+        * @param array $groups Array of Strings List of internal group names
+        * @return array Array of Strings List of permission key names for given groups combined
+        */
+       public static function getGroupPermissions( $groups ) {
+               global $wgGroupPermissions, $wgRevokePermissions;
+               $rights = array();
+               // grant every granted permission first
+               foreach ( $groups as $group ) {
+                       if ( isset( $wgGroupPermissions[$group] ) ) {
+                               $rights = array_merge( $rights,
+                                       // array_filter removes empty items
+                                       array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
+                       }
+               }
+               // now revoke the revoked permissions
+               foreach ( $groups as $group ) {
+                       if ( isset( $wgRevokePermissions[$group] ) ) {
+                               $rights = array_diff( $rights,
+                                       array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
+                       }
+               }
+               return array_unique( $rights );
+       }
+
+       /**
+        * Get all the groups who have a given permission
+        *
+        * @param string $role Role to check
+        * @return array Array of Strings List of internal group names with the given permission
+        */
+       public static function getGroupsWithPermission( $role ) {
+               global $wgGroupPermissions;
+               $allowedGroups = array();
+               foreach ( array_keys( $wgGroupPermissions ) as $group ) {
+                       if ( self::groupHasPermission( $group, $role ) ) {
+                               $allowedGroups[] = $group;
+                       }
+               }
+               return $allowedGroups;
+       }
+
+       /**
+        * Check, if the given group has the given permission
+        *
+        * If you're wanting to check whether all users have a permission, use
+        * User::isEveryoneAllowed() instead. That properly checks if it's revoked
+        * from anyone.
+        *
+        * @since 1.21
+        * @param string $group Group to check
+        * @param string $role Role to check
+        * @return bool
+        */
+       public static function groupHasPermission( $group, $role ) {
+               global $wgGroupPermissions, $wgRevokePermissions;
+               return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
+                       && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
+       }
+
+       /**
+        * Check if all users have the given permission
+        *
+        * @since 1.22
+        * @param string $right Right to check
+        * @return bool
+        */
+       public static function isEveryoneAllowed( $right ) {
+               global $wgGroupPermissions, $wgRevokePermissions;
+               static $cache = array();
+
+               // Use the cached results, except in unit tests which rely on
+               // being able change the permission mid-request
+               if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       return $cache[$right];
+               }
+
+               if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
+                       $cache[$right] = false;
+                       return false;
+               }
+
+               // If it's revoked anywhere, then everyone doesn't have it
+               foreach ( $wgRevokePermissions as $rights ) {
+                       if ( isset( $rights[$right] ) && $rights[$right] ) {
+                               $cache[$right] = false;
+                               return false;
+                       }
+               }
+
+               // Allow extensions (e.g. OAuth) to say false
+               if ( !Hooks::run( 'UserIsEveryoneAllowed', array( $right ) ) ) {
+                       $cache[$right] = false;
+                       return false;
+               }
+
+               $cache[$right] = true;
+               return true;
+       }
+
+       /**
+        * Get the localized descriptive name for a group, if it exists
+        *
+        * @param string $group Internal group name
+        * @return string Localized descriptive group name
+        */
+       public static function getGroupName( $group ) {
+               $msg = wfMessage( "group-$group" );
+               return $msg->isBlank() ? $group : $msg->text();
+       }
+
+       /**
+        * Get the localized descriptive name for a member of a group, if it exists
+        *
+        * @param string $group Internal group name
+        * @param string $username Username for gender (since 1.19)
+        * @return string Localized name for group member
+        */
+       public static function getGroupMember( $group, $username = '#' ) {
+               $msg = wfMessage( "group-$group-member", $username );
+               return $msg->isBlank() ? $group : $msg->text();
+       }
+
+       /**
+        * Return the set of defined explicit groups.
+        * The implicit groups (by default *, 'user' and 'autoconfirmed')
+        * are not included, as they are defined automatically, not in the database.
+        * @return array Array of internal group names
+        */
+       public static function getAllGroups() {
+               global $wgGroupPermissions, $wgRevokePermissions;
+               return array_diff(
+                       array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
+                       self::getImplicitGroups()
+               );
+       }
+
+       /**
+        * Get a list of all available permissions.
+        * @return string[] Array of permission names
+        */
+       public static function getAllRights() {
+               if ( self::$mAllRights === false ) {
+                       global $wgAvailableRights;
+                       if ( count( $wgAvailableRights ) ) {
+                               self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
+                       } else {
+                               self::$mAllRights = self::$mCoreRights;
+                       }
+                       Hooks::run( 'UserGetAllRights', array( &self::$mAllRights ) );
+               }
+               return self::$mAllRights;
+       }
+
+       /**
+        * Get a list of implicit groups
+        * @return array Array of Strings Array of internal group names
+        */
+       public static function getImplicitGroups() {
+               global $wgImplicitGroups;
+
+               $groups = $wgImplicitGroups;
+               # Deprecated, use $wgImplicitGroups instead
+               Hooks::run( 'UserGetImplicitGroups', array( &$groups ), '1.25' );
+
+               return $groups;
+       }
+
+       /**
+        * Get the title of a page describing a particular group
+        *
+        * @param string $group Internal group name
+        * @return Title|bool Title of the page if it exists, false otherwise
+        */
+       public static function getGroupPage( $group ) {
+               $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
+               if ( $msg->exists() ) {
+                       $title = Title::newFromText( $msg->text() );
+                       if ( is_object( $title ) ) {
+                               return $title;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Create a link to the group in HTML, if available;
+        * else return the group name.
+        *
+        * @param string $group Internal name of the group
+        * @param string $text The text of the link
+        * @return string HTML link to the group
+        */
+       public static function makeGroupLinkHTML( $group, $text = '' ) {
+               if ( $text == '' ) {
+                       $text = self::getGroupName( $group );
+               }
+               $title = self::getGroupPage( $group );
+               if ( $title ) {
+                       return Linker::link( $title, htmlspecialchars( $text ) );
+               } else {
+                       return htmlspecialchars( $text );
+               }
+       }
+
+       /**
+        * Create a link to the group in Wikitext, if available;
+        * else return the group name.
+        *
+        * @param string $group Internal name of the group
+        * @param string $text The text of the link
+        * @return string Wikilink to the group
+        */
+       public static function makeGroupLinkWiki( $group, $text = '' ) {
+               if ( $text == '' ) {
+                       $text = self::getGroupName( $group );
+               }
+               $title = self::getGroupPage( $group );
+               if ( $title ) {
+                       $page = $title->getFullText();
+                       return "[[$page|$text]]";
+               } else {
+                       return $text;
+               }
+       }
+
+       /**
+        * Returns an array of the groups that a particular group can add/remove.
+        *
+        * @param string $group The group to check for whether it can add/remove
+        * @return array Array( 'add' => array( addablegroups ),
+        *     'remove' => array( removablegroups ),
+        *     'add-self' => array( addablegroups to self),
+        *     'remove-self' => array( removable groups from self) )
+        */
+       public static function changeableByGroup( $group ) {
+               global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+               $groups = array(
+                       'add' => array(),
+                       'remove' => array(),
+                       'add-self' => array(),
+                       'remove-self' => array()
+               );
+
+               if ( empty( $wgAddGroups[$group] ) ) {
+                       // Don't add anything to $groups
+               } elseif ( $wgAddGroups[$group] === true ) {
+                       // You get everything
+                       $groups['add'] = self::getAllGroups();
+               } elseif ( is_array( $wgAddGroups[$group] ) ) {
+                       $groups['add'] = $wgAddGroups[$group];
+               }
+
+               // Same thing for remove
+               if ( empty( $wgRemoveGroups[$group] ) ) {
+                       // Do nothing
+               } elseif ( $wgRemoveGroups[$group] === true ) {
+                       $groups['remove'] = self::getAllGroups();
+               } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
+                       $groups['remove'] = $wgRemoveGroups[$group];
+               }
+
+               // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
+               if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
+                       foreach ( $wgGroupsAddToSelf as $key => $value ) {
+                               if ( is_int( $key ) ) {
+                                       $wgGroupsAddToSelf['user'][] = $value;
+                               }
+                       }
+               }
+
+               if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
+                       foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
+                               if ( is_int( $key ) ) {
+                                       $wgGroupsRemoveFromSelf['user'][] = $value;
+                               }
+                       }
+               }
+
+               // Now figure out what groups the user can add to him/herself
+               if ( empty( $wgGroupsAddToSelf[$group] ) ) {
+                       // Do nothing
+               } elseif ( $wgGroupsAddToSelf[$group] === true ) {
+                       // No idea WHY this would be used, but it's there
+                       $groups['add-self'] = User::getAllGroups();
+               } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
+                       $groups['add-self'] = $wgGroupsAddToSelf[$group];
+               }
+
+               if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
+                       // Do nothing
+               } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
+                       $groups['remove-self'] = User::getAllGroups();
+               } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
+                       $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
+               }
+
+               return $groups;
+       }
+
+       /**
+        * Returns an array of groups that this user can add and remove
+        * @return array Array( 'add' => array( addablegroups ),
+        *  'remove' => array( removablegroups ),
+        *  'add-self' => array( addablegroups to self),
+        *  'remove-self' => array( removable groups from self) )
+        */
+       public function changeableGroups() {
+               if ( $this->isAllowed( 'userrights' ) ) {
+                       // This group gives the right to modify everything (reverse-
+                       // compatibility with old "userrights lets you change
+                       // everything")
+                       // Using array_merge to make the groups reindexed
+                       $all = array_merge( User::getAllGroups() );
+                       return array(
+                               'add' => $all,
+                               'remove' => $all,
+                               'add-self' => array(),
+                               'remove-self' => array()
+                       );
+               }
+
+               // Okay, it's not so simple, we will have to go through the arrays
+               $groups = array(
+                       'add' => array(),
+                       'remove' => array(),
+                       'add-self' => array(),
+                       'remove-self' => array()
+               );
+               $addergroups = $this->getEffectiveGroups();
+
+               foreach ( $addergroups as $addergroup ) {
+                       $groups = array_merge_recursive(
+                               $groups, $this->changeableByGroup( $addergroup )
+                       );
+                       $groups['add'] = array_unique( $groups['add'] );
+                       $groups['remove'] = array_unique( $groups['remove'] );
+                       $groups['add-self'] = array_unique( $groups['add-self'] );
+                       $groups['remove-self'] = array_unique( $groups['remove-self'] );
+               }
+               return $groups;
+       }
+
+       /**
+        * Deferred version of incEditCountImmediate()
+        */
+       public function incEditCount() {
+               $that = $this;
+               wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $that ) {
+                       $that->incEditCountImmediate();
+               } );
+       }
+
+       /**
+        * Increment the user's edit-count field.
+        * Will have no effect for anonymous users.
+        * @since 1.26
+        */
+       public function incEditCountImmediate() {
+               if ( $this->isAnon() ) {
+                       return;
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               // No rows will be "affected" if user_editcount is NULL
+               $dbw->update(
+                       'user',
+                       array( 'user_editcount=user_editcount+1' ),
+                       array( 'user_id' => $this->getId(), 'user_editcount IS NOT NULL' ),
+                       __METHOD__
+               );
+               // Lazy initialization check...
+               if ( $dbw->affectedRows() == 0 ) {
+                       // Now here's a goddamn hack...
+                       $dbr = wfGetDB( DB_SLAVE );
+                       if ( $dbr !== $dbw ) {
+                               // If we actually have a slave server, the count is
+                               // at least one behind because the current transaction
+                               // has not been committed and replicated.
+                               $this->initEditCount( 1 );
+                       } else {
+                               // But if DB_SLAVE is selecting the master, then the
+                               // count we just read includes the revision that was
+                               // just added in the working transaction.
+                               $this->initEditCount();
+                       }
+               }
+               // Edit count in user cache too
+               $this->invalidateCache();
+       }
+
+       /**
+        * Initialize user_editcount from data out of the revision table
+        *
+        * @param int $add Edits to add to the count from the revision table
+        * @return int Number of edits
+        */
+       protected function initEditCount( $add = 0 ) {
+               // Pull from a slave to be less cruel to servers
+               // Accuracy isn't the point anyway here
+               $dbr = wfGetDB( DB_SLAVE );
+               $count = (int)$dbr->selectField(
+                       'revision',
+                       'COUNT(rev_user)',
+                       array( 'rev_user' => $this->getId() ),
+                       __METHOD__
+               );
+               $count = $count + $add;
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'user',
+                       array( 'user_editcount' => $count ),
+                       array( 'user_id' => $this->getId() ),
+                       __METHOD__
+               );
+
+               return $count;
+       }
+
+       /**
+        * Get the description of a given right
+        *
+        * @param string $right Right to query
+        * @return string Localized description of the right
+        */
+       public static function getRightDescription( $right ) {
+               $key = "right-$right";
+               $msg = wfMessage( $key );
+               return $msg->isBlank() ? $right : $msg->text();
+       }
+
+       /**
+        * Make a new-style password hash
+        *
+        * @param string $password Plain-text password
+        * @param bool|string $salt Optional salt, may be random or the user ID.
+        *  If unspecified or false, will generate one automatically
+        * @return string Password hash
+        * @deprecated since 1.24, use Password class
+        */
+       public static function crypt( $password, $salt = false ) {
+               wfDeprecated( __METHOD__, '1.24' );
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $hash = $passwordFactory->newFromPlaintext( $password );
+               return $hash->toString();
+       }
+
+       /**
+        * Compare a password hash with a plain-text password. Requires the user
+        * ID if there's a chance that the hash is an old-style hash.
+        *
+        * @param string $hash Password hash
+        * @param string $password Plain-text password to compare
+        * @param string|bool $userId User ID for old-style password salt
+        *
+        * @return bool
+        * @deprecated since 1.24, use Password class
+        */
+       public static function comparePasswords( $hash, $password, $userId = false ) {
+               wfDeprecated( __METHOD__, '1.24' );
+
+               // Check for *really* old password hashes that don't even have a type
+               // The old hash format was just an md5 hex hash, with no type information
+               if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
+                       global $wgPasswordSalt;
+                       if ( $wgPasswordSalt ) {
+                               $password = ":B:{$userId}:{$hash}";
+                       } else {
+                               $password = ":A:{$hash}";
+                       }
+               }
+
+               $passwordFactory = new PasswordFactory();
+               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+               $hash = $passwordFactory->newFromCiphertext( $hash );
+               return $hash->equals( $password );
+       }
+
+       /**
+        * Add a newuser log entry for this user.
+        * Before 1.19 the return value was always true.
+        *
+        * @param string|bool $action Account creation type.
+        *   - String, one of the following values:
+        *     - 'create' for an anonymous user creating an account for himself.
+        *       This will force the action's performer to be the created user itself,
+        *       no matter the value of $wgUser
+        *     - 'create2' for a logged in user creating an account for someone else
+        *     - 'byemail' when the created user will receive its password by e-mail
+        *     - 'autocreate' when the user is automatically created (such as by CentralAuth).
+        *   - Boolean means whether the account was created by e-mail (deprecated):
+        *     - true will be converted to 'byemail'
+        *     - false will be converted to 'create' if this object is the same as
+        *       $wgUser and to 'create2' otherwise
+        *
+        * @param string $reason User supplied reason
+        *
+        * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
+        */
+       public function addNewUserLogEntry( $action = false, $reason = '' ) {
+               global $wgUser, $wgNewUserLog;
+               if ( empty( $wgNewUserLog ) ) {
+                       return true; // disabled
+               }
+
+               if ( $action === true ) {
+                       $action = 'byemail';
+               } elseif ( $action === false ) {
+                       if ( $this->equals( $wgUser ) ) {
+                               $action = 'create';
+                       } else {
+                               $action = 'create2';
+                       }
+               }
+
+               if ( $action === 'create' || $action === 'autocreate' ) {
+                       $performer = $this;
+               } else {
+                       $performer = $wgUser;
+               }
+
+               $logEntry = new ManualLogEntry( 'newusers', $action );
+               $logEntry->setPerformer( $performer );
+               $logEntry->setTarget( $this->getUserPage() );
+               $logEntry->setComment( $reason );
+               $logEntry->setParameters( array(
+                       '4::userid' => $this->getId(),
+               ) );
+               $logid = $logEntry->insert();
+
+               if ( $action !== 'autocreate' ) {
+                       $logEntry->publish( $logid );
+               }
+
+               return (int)$logid;
+       }
+
+       /**
+        * Add an autocreate newuser log entry for this user
+        * Used by things like CentralAuth and perhaps other authplugins.
+        * Consider calling addNewUserLogEntry() directly instead.
+        *
+        * @return bool
+        */
+       public function addNewUserLogEntryAutoCreate() {
+               $this->addNewUserLogEntry( 'autocreate' );
+
+               return true;
+       }
+
+       /**
+        * Load the user options either from cache, the database or an array
+        *
+        * @param array $data Rows for the current user out of the user_properties table
+        */
+       protected function loadOptions( $data = null ) {
+               global $wgContLang;
+
+               $this->load();
+
+               if ( $this->mOptionsLoaded ) {
+                       return;
+               }
+
+               $this->mOptions = self::getDefaultOptions();
+
+               if ( !$this->getId() ) {
+                       // For unlogged-in users, load language/variant options from request.
+                       // There's no need to do it for logged-in users: they can set preferences,
+                       // and handling of page content is done by $pageLang->getPreferredVariant() and such,
+                       // so don't override user's choice (especially when the user chooses site default).
+                       $variant = $wgContLang->getDefaultVariant();
+                       $this->mOptions['variant'] = $variant;
+                       $this->mOptions['language'] = $variant;
+                       $this->mOptionsLoaded = true;
+                       return;
+               }
+
+               // Maybe load from the object
+               if ( !is_null( $this->mOptionOverrides ) ) {
+                       wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
+                       foreach ( $this->mOptionOverrides as $key => $value ) {
+                               $this->mOptions[$key] = $value;
+                       }
+               } else {
+                       if ( !is_array( $data ) ) {
+                               wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
+                               // Load from database
+                               $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
+                                       ? wfGetDB( DB_MASTER )
+                                       : wfGetDB( DB_SLAVE );
+
+                               $res = $dbr->select(
+                                       'user_properties',
+                                       array( 'up_property', 'up_value' ),
+                                       array( 'up_user' => $this->getId() ),
+                                       __METHOD__
+                               );
+
+                               $this->mOptionOverrides = array();
+                               $data = array();
+                               foreach ( $res as $row ) {
+                                       $data[$row->up_property] = $row->up_value;
+                               }
+                       }
+                       foreach ( $data as $property => $value ) {
+                               $this->mOptionOverrides[$property] = $value;
+                               $this->mOptions[$property] = $value;
+                       }
+               }
+
+               $this->mOptionsLoaded = true;
+
+               Hooks::run( 'UserLoadOptions', array( $this, &$this->mOptions ) );
+       }
+
+       /**
+        * Saves the non-default options for this user, as previously set e.g. via
+        * setOption(), in the database's "user_properties" (preferences) table.
+        * Usually used via saveSettings().
+        */
+       protected function saveOptions() {
+               $this->loadOptions();
+
+               // Not using getOptions(), to keep hidden preferences in database
+               $saveOptions = $this->mOptions;
+
+               // Allow hooks to abort, for instance to save to a global profile.
+               // Reset options to default state before saving.
+               if ( !Hooks::run( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
+                       return;
+               }
+
+               $userId = $this->getId();
+
+               $insert_rows = array(); // all the new preference rows
+               foreach ( $saveOptions as $key => $value ) {
+                       // Don't bother storing default values
+                       $defaultOption = self::getDefaultOption( $key );
+                       if ( ( $defaultOption === null && $value !== false && $value !== null )
+                               || $value != $defaultOption
+                       ) {
+                               $insert_rows[] = array(
+                                       'up_user' => $userId,
+                                       'up_property' => $key,
+                                       'up_value' => $value,
+                               );
+                       }
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               $res = $dbw->select( 'user_properties',
+                       array( 'up_property', 'up_value' ), array( 'up_user' => $userId ), __METHOD__ );
+
+               // Find prior rows that need to be removed or updated. These rows will
+               // all be deleted (the later so that INSERT IGNORE applies the new values).
+               $keysDelete = array();
+               foreach ( $res as $row ) {
+                       if ( !isset( $saveOptions[$row->up_property] )
+                               || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
+                       ) {
+                               $keysDelete[] = $row->up_property;
+                       }
+               }
+
+               if ( count( $keysDelete ) ) {
+                       // Do the DELETE by PRIMARY KEY for prior rows.
+                       // In the past a very large portion of calls to this function are for setting
+                       // 'rememberpassword' for new accounts (a preference that has since been removed).
+                       // Doing a blanket per-user DELETE for new accounts with no rows in the table
+                       // caused gap locks on [max user ID,+infinity) which caused high contention since
+                       // updates would pile up on each other as they are for higher (newer) user IDs.
+                       // It might not be necessary these days, but it shouldn't hurt either.
+                       $dbw->delete( 'user_properties',
+                               array( 'up_user' => $userId, 'up_property' => $keysDelete ), __METHOD__ );
+               }
+               // Insert the new preference rows
+               $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
+       }
+
+       /**
+        * Lazily instantiate and return a factory object for making passwords
+        *
+        * @deprecated since 1.27, create a PasswordFactory directly instead
+        * @return PasswordFactory
+        */
+       public static function getPasswordFactory() {
+               wfDeprecated( __METHOD__, '1.27' );
+               $ret = new PasswordFactory();
+               $ret->init( RequestContext::getMain()->getConfig() );
+               return $ret;
+       }
+
+       /**
+        * Provide an array of HTML5 attributes to put on an input element
+        * intended for the user to enter a new password.  This may include
+        * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
+        *
+        * Do *not* use this when asking the user to enter his current password!
+        * Regardless of configuration, users may have invalid passwords for whatever
+        * reason (e.g., they were set before requirements were tightened up).
+        * Only use it when asking for a new password, like on account creation or
+        * ResetPass.
+        *
+        * Obviously, you still need to do server-side checking.
+        *
+        * NOTE: A combination of bugs in various browsers means that this function
+        * actually just returns array() unconditionally at the moment.  May as
+        * well keep it around for when the browser bugs get fixed, though.
+        *
+        * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
+        *
+        * @deprecated since 1.27
+        * @return array Array of HTML attributes suitable for feeding to
+        *   Html::element(), directly or indirectly.  (Don't feed to Xml::*()!
+        *   That will get confused by the boolean attribute syntax used.)
+        */
+       public static function passwordChangeInputAttribs() {
+               global $wgMinimalPasswordLength;
+
+               if ( $wgMinimalPasswordLength == 0 ) {
+                       return array();
+               }
+
+               # Note that the pattern requirement will always be satisfied if the
+               # input is empty, so we need required in all cases.
+
+               # @todo FIXME: Bug 23769: This needs to not claim the password is required
+               # if e-mail confirmation is being used.  Since HTML5 input validation
+               # is b0rked anyway in some browsers, just return nothing.  When it's
+               # re-enabled, fix this code to not output required for e-mail
+               # registration.
+               # $ret = array( 'required' );
+               $ret = array();
+
+               # We can't actually do this right now, because Opera 9.6 will print out
+               # the entered password visibly in its error message!  When other
+               # browsers add support for this attribute, or Opera fixes its support,
+               # we can add support with a version check to avoid doing this on Opera
+               # versions where it will be a problem.  Reported to Opera as
+               # DSK-262266, but they don't have a public bug tracker for us to follow.
+               /*
+               if ( $wgMinimalPasswordLength > 1 ) {
+                       $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
+                       $ret['title'] = wfMessage( 'passwordtooshort' )
+                               ->numParams( $wgMinimalPasswordLength )->text();
+               }
+               */
+
+               return $ret;
+       }
+
+       /**
+        * Return the list of user fields that should be selected to create
+        * a new user object.
+        * @return array
+        */
+       public static function selectFields() {
+               return array(
+                       'user_id',
+                       'user_name',
+                       'user_real_name',
+                       'user_email',
+                       'user_touched',
+                       'user_token',
+                       'user_email_authenticated',
+                       'user_email_token',
+                       'user_email_token_expires',
+                       'user_registration',
+                       'user_editcount',
+               );
+       }
+
+       /**
+        * Factory function for fatal permission-denied errors
+        *
+        * @since 1.22
+        * @param string $permission User right required
+        * @return Status
+        */
+       static function newFatalPermissionDeniedStatus( $permission ) {
+               global $wgLang;
+
+               $groups = array_map(
+                       array( 'User', 'makeGroupLinkWiki' ),
+                       User::getGroupsWithPermission( $permission )
+               );
+
+               if ( $groups ) {
+                       return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
+               } else {
+                       return Status::newFatal( 'badaccess-group0' );
+               }
+       }
+
+       /**
+        * Checks if two user objects point to the same user.
+        *
+        * @since 1.25
+        * @param User $user
+        * @return bool
+        */
+       public function equals( User $user ) {
+               return $this->getName() === $user->getName();
+       }
+}
diff --git a/includes/user/UserArray.php b/includes/user/UserArray.php
new file mode 100644 (file)
index 0000000..31bd601
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Class to walk into a list of User objects.
+ *
+ * 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
+ */
+
+abstract class UserArray implements Iterator {
+       /**
+        * @param ResultWrapper $res
+        * @return UserArrayFromResult
+        */
+       static function newFromResult( $res ) {
+               $userArray = null;
+               if ( !Hooks::run( 'UserArrayFromResult', array( &$userArray, $res ) ) ) {
+                       return null;
+               }
+               if ( $userArray === null ) {
+                       $userArray = self::newFromResult_internal( $res );
+               }
+               return $userArray;
+       }
+
+       /**
+        * @param array $ids
+        * @return UserArrayFromResult
+        */
+       static function newFromIDs( $ids ) {
+               $ids = array_map( 'intval', (array)$ids ); // paranoia
+               if ( !$ids ) {
+                       // Database::select() doesn't like empty arrays
+                       return new ArrayIterator( array() );
+               }
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select(
+                       'user',
+                       User::selectFields(),
+                       array( 'user_id' => array_unique( $ids ) ),
+                       __METHOD__
+               );
+               return self::newFromResult( $res );
+       }
+
+       /**
+        * @since 1.25
+        * @param array $names
+        * @return UserArrayFromResult
+        */
+       static function newFromNames( $names ) {
+               $names = array_map( 'strval', (array)$names ); // paranoia
+               if ( !$names ) {
+                       // Database::select() doesn't like empty arrays
+                       return new ArrayIterator( array() );
+               }
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select(
+                       'user',
+                       User::selectFields(),
+                       array( 'user_name' => array_unique( $names ) ),
+                       __METHOD__
+               );
+               return self::newFromResult( $res );
+       }
+
+       /**
+        * @param ResultWrapper $res
+        * @return UserArrayFromResult
+        */
+       protected static function newFromResult_internal( $res ) {
+               return new UserArrayFromResult( $res );
+       }
+}
diff --git a/includes/user/UserArrayFromResult.php b/includes/user/UserArrayFromResult.php
new file mode 100644 (file)
index 0000000..fb533d0
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Class to walk into a list of User objects.
+ *
+ * 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
+ */
+
+class UserArrayFromResult extends UserArray implements Countable {
+       /** @var ResultWrapper */
+       public $res;
+
+       /** @var int */
+       public $key;
+
+       /** @var bool|stdClass */
+       public $current;
+
+       /**
+        * @param ResultWrapper $res
+        */
+       function __construct( $res ) {
+               $this->res = $res;
+               $this->key = 0;
+               $this->setCurrent( $this->res->current() );
+       }
+
+       /**
+        * @param bool|stdClass $row
+        * @return void
+        */
+       protected function setCurrent( $row ) {
+               if ( $row === false ) {
+                       $this->current = false;
+               } else {
+                       $this->current = User::newFromRow( $row );
+               }
+       }
+
+       /**
+        * @return int
+        */
+       public function count() {
+               return $this->res->numRows();
+       }
+
+       /**
+        * @return User
+        */
+       function current() {
+               return $this->current;
+       }
+
+       function key() {
+               return $this->key;
+       }
+
+       function next() {
+               $row = $this->res->next();
+               $this->setCurrent( $row );
+               $this->key++;
+       }
+
+       function rewind() {
+               $this->res->rewind();
+               $this->key = 0;
+               $this->setCurrent( $this->res->current() );
+       }
+
+       /**
+        * @return bool
+        */
+       function valid() {
+               return $this->current !== false;
+       }
+}
diff --git a/includes/user/UserRightsProxy.php b/includes/user/UserRightsProxy.php
new file mode 100644 (file)
index 0000000..e686ae3
--- /dev/null
@@ -0,0 +1,287 @@
+<?php
+/**
+ * Representation of an user on a other locally-hosted wiki.
+ *
+ * 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
+ */
+
+/**
+ * Cut-down copy of User interface for local-interwiki-database
+ * user rights manipulation.
+ */
+class UserRightsProxy {
+
+       /**
+        * Constructor.
+        *
+        * @see newFromId()
+        * @see newFromName()
+        * @param IDatabase $db Db connection
+        * @param string $database Database name
+        * @param string $name User name
+        * @param int $id User ID
+        */
+       private function __construct( $db, $database, $name, $id ) {
+               $this->db = $db;
+               $this->database = $database;
+               $this->name = $name;
+               $this->id = intval( $id );
+               $this->newOptions = array();
+       }
+
+       /**
+        * Accessor for $this->database
+        *
+        * @return string Database name
+        */
+       public function getDBName() {
+               return $this->database;
+       }
+
+       /**
+        * Confirm the selected database name is a valid local interwiki database name.
+        *
+        * @param string $database Database name
+        * @return bool
+        */
+       public static function validDatabase( $database ) {
+               global $wgLocalDatabases;
+               return in_array( $database, $wgLocalDatabases );
+       }
+
+       /**
+        * Same as User::whoIs()
+        *
+        * @param string $database Database name
+        * @param int $id User ID
+        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
+        * @return string User name or false if the user doesn't exist
+        */
+       public static function whoIs( $database, $id, $ignoreInvalidDB = false ) {
+               $user = self::newFromId( $database, $id, $ignoreInvalidDB );
+               if ( $user ) {
+                       return $user->name;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Factory function; get a remote user entry by ID number.
+        *
+        * @param string $database Database name
+        * @param int $id User ID
+        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
+        * @return UserRightsProxy|null If doesn't exist
+        */
+       public static function newFromId( $database, $id, $ignoreInvalidDB = false ) {
+               return self::newFromLookup( $database, 'user_id', intval( $id ), $ignoreInvalidDB );
+       }
+
+       /**
+        * Factory function; get a remote user entry by name.
+        *
+        * @param string $database Database name
+        * @param string $name User name
+        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
+        * @return UserRightsProxy|null If doesn't exist
+        */
+       public static function newFromName( $database, $name, $ignoreInvalidDB = false ) {
+               return self::newFromLookup( $database, 'user_name', $name, $ignoreInvalidDB );
+       }
+
+       /**
+        * @param string $database
+        * @param string $field
+        * @param string $value
+        * @param bool $ignoreInvalidDB
+        * @return null|UserRightsProxy
+        */
+       private static function newFromLookup( $database, $field, $value, $ignoreInvalidDB = false ) {
+               global $wgSharedDB, $wgSharedTables;
+               // If the user table is shared, perform the user query on it,
+               // but don't pass it to the UserRightsProxy,
+               // as user rights are normally not shared.
+               if ( $wgSharedDB && in_array( 'user', $wgSharedTables ) ) {
+                       $userdb = self::getDB( $wgSharedDB, $ignoreInvalidDB );
+               } else {
+                       $userdb = self::getDB( $database, $ignoreInvalidDB );
+               }
+
+               $db = self::getDB( $database, $ignoreInvalidDB );
+
+               if ( $db && $userdb ) {
+                       $row = $userdb->selectRow( 'user',
+                               array( 'user_id', 'user_name' ),
+                               array( $field => $value ),
+                               __METHOD__ );
+
+                       if ( $row !== false ) {
+                               return new UserRightsProxy( $db, $database,
+                                       $row->user_name,
+                                       intval( $row->user_id ) );
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Open a database connection to work on for the requested user.
+        * This may be a new connection to another database for remote users.
+        *
+        * @param string $database
+        * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
+        * @return IDatabase|null If invalid selection
+        */
+       public static function getDB( $database, $ignoreInvalidDB = false ) {
+               global $wgDBname;
+               if ( $ignoreInvalidDB || self::validDatabase( $database ) ) {
+                       if ( $database == $wgDBname ) {
+                               // Hmm... this shouldn't happen though. :)
+                               return wfGetDB( DB_MASTER );
+                       } else {
+                               return wfGetDB( DB_MASTER, array(), $database );
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * @return int
+        */
+       public function getId() {
+               return $this->id;
+       }
+
+       /**
+        * @return bool
+        */
+       public function isAnon() {
+               return $this->getId() == 0;
+       }
+
+       /**
+        * Same as User::getName()
+        *
+        * @return string
+        */
+       public function getName() {
+               return $this->name . '@' . $this->database;
+       }
+
+       /**
+        * Same as User::getUserPage()
+        *
+        * @return Title
+        */
+       public function getUserPage() {
+               return Title::makeTitle( NS_USER, $this->getName() );
+       }
+
+       /**
+        * Replaces User::getUserGroups()
+        * @return array
+        */
+       function getGroups() {
+               $res = $this->db->select( 'user_groups',
+                       array( 'ug_group' ),
+                       array( 'ug_user' => $this->id ),
+                       __METHOD__ );
+               $groups = array();
+               foreach ( $res as $row ) {
+                       $groups[] = $row->ug_group;
+               }
+               return $groups;
+       }
+
+       /**
+        * Replaces User::addUserGroup()
+        * @param string $group
+        *
+        * @return bool
+        */
+       function addGroup( $group ) {
+               $this->db->insert( 'user_groups',
+                       array(
+                               'ug_user' => $this->id,
+                               'ug_group' => $group,
+                       ),
+                       __METHOD__,
+                       array( 'IGNORE' ) );
+
+               return true;
+       }
+
+       /**
+        * Replaces User::removeUserGroup()
+        * @param string $group
+        *
+        * @return bool
+        */
+       function removeGroup( $group ) {
+               $this->db->delete( 'user_groups',
+                       array(
+                               'ug_user' => $this->id,
+                               'ug_group' => $group,
+                       ),
+                       __METHOD__ );
+
+               return true;
+       }
+
+       /**
+        * Replaces User::setOption()
+        * @param string $option
+        * @param mixed $value
+        */
+       public function setOption( $option, $value ) {
+               $this->newOptions[$option] = $value;
+       }
+
+       public function saveSettings() {
+               $rows = array();
+               foreach ( $this->newOptions as $option => $value ) {
+                       $rows[] = array(
+                               'up_user' => $this->id,
+                               'up_property' => $option,
+                               'up_value' => $value,
+                       );
+               }
+               $this->db->replace( 'user_properties',
+                       array( array( 'up_user', 'up_property' ) ),
+                       $rows, __METHOD__
+               );
+               $this->invalidateCache();
+       }
+
+       /**
+        * Replaces User::touchUser()
+        */
+       function invalidateCache() {
+               $this->db->update( 'user',
+                       array( 'user_touched' => $this->db->timestamp() ),
+                       array( 'user_id' => $this->id ),
+                       __METHOD__ );
+
+               $wikiId = $this->db->getWikiID();
+               $userId = $this->id;
+               $this->db->onTransactionPreCommitOrIdle( function() use ( $wikiId, $userId ) {
+                       User::purge( $wikiId, $userId );
+               } );
+       }
+}
index d19ea49..96f80b5 100644 (file)
        "prefs-help-recentchangescount": "بما في ذلك أحدث التغييرات وتاريخ الصفحات والسجلات.",
        "prefs-help-watchlist-token2": "هذا هو المفتاح السري لتغذية الوِب لقائمة مراقبتك.\nيمكن لأي شخص يعرفه أن يقرأ قائمة مراقبتك، ولذا لا تتشاركه مع أحد. [[Special:ResetTokens|انقر هنا إذا أردت إعادة ضبطه]].",
        "savedprefs": "تم حفظ تفضيلاتك.",
+       "savedrights": "حُفظت الصلاحيات الجديدة {{GENDER:$1|للمستخدم|للمستخدمة}} $1.",
        "timezonelegend": "المنطقة الزمنية:",
        "localtime": "الوقت المحلي:",
        "timezoneuseserverdefault": "استخدام الويكي الافتراضي ($1)",
index 3e5d53d..16cfc9b 100644 (file)
@@ -26,7 +26,8 @@
                        "Arystanbek",
                        "Dağlı95",
                        "Sayginer",
-                       "Şeyx Şamil"
+                       "Şeyx Şamil",
+                       "Serkanland"
                ]
        },
        "tog-underline": "Keçidlərin altını xətlə:",
        "unwatching": "İzlənilmir...",
        "enotif_reset": "Baxılmış bütün səhifələri işarələ.",
        "enotif_impersonal_salutation": "{{SITENAME}} istifadəçisi",
+       "enotif_body_intro_moved": "{{SITENAME}}da  $1 səhifəsinin adı $PAGEEDITDATE tarixində $2 tərəfindən {{GENDER:$2|dəyişdirilib}}, son versiya üçün bura baxın: $3.",
        "enotif_lastvisited": "Sonuncu ziyarətinizdən indiyədək olan bütün dəyişiklikləri görmək üçün baxın: $1.",
        "enotif_lastdiff": "Bu dəyişikliyi görmək üçün $1 səhifəsinə baxın.",
        "enotif_anon_editor": "qeydiyyatsız istifadəçi $1",
index eacacab..992633b 100644 (file)
        "morenotlisted": "Гэта ня поўны сьпіс.",
        "mypage": "Старонка",
        "mytalk": "Гутаркі",
-       "anontalk": "Гутаркі для гэтага IP-адрасу",
+       "anontalk": "Гутаркі",
        "navigation": "Навігацыя",
        "and": "&#32;і",
        "qbfind": "Знайсьці",
        "contributions": "Унёсак {{GENDER:$1|удзельніка|удзельніцы}}",
        "contributions-title": "Унёсак {{GENDER:$1|удзельніка|удзельніцы}} $1",
        "mycontris": "Унёсак",
+       "anoncontribs": "Унёсак",
        "contribsub2": "Для {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Рахунак удзельніка «$1» не зарэгістраваны.",
        "nocontribs": "Ня знойдзена зьменаў, якія адпавядаюць гэтым крытэрыям.",
        "tooltip-pt-preferences": "Вашыя налады",
        "tooltip-pt-watchlist": "Сьпіс старонак, за зьменамі якіх Вы назіраеце",
        "tooltip-pt-mycontris": "Ваш унёсак",
+       "tooltip-pt-anoncontribs": "Сьпіс рэдагаваньняў, зробленых з гэтага IP-адрасу",
        "tooltip-pt-login": "Вас запрашаюць увайсьці, хаця гэта і неабавязкова.",
        "tooltip-pt-logout": "Выйсьці",
        "tooltip-pt-createaccount": "Мы прапануем вам стварыць рахунак і ўвайсьці, але гэта не абавязкова",
index 0436c8b..ac0204f 100644 (file)
        "contributions": "{{GENDER:$1|کار زوروک}} ئی شراکت ئان",
        "contributions-title": "$1 ئی کار زوروکئ شراکت ئان",
        "mycontris": "شراکت ئان",
+       "anoncontribs": "شراکت ئان",
        "contribsub2": "په {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "«$1» ئی کار زوروکین حساب راجستر نه بوته.",
        "nocontribs": "هیچ تغیری گۆ ای مشخصات ئان ودێ نه بوت",
index b8ba0e2..dbf5324 100644 (file)
        "revdelete-show-file-submit": "হ্যাঁ",
        "revdelete-selected-text": "[[:$2]] পাতার {{PLURAL:$1|নির্বাচিত সংশোধন|নির্বাচিত সংশোধনসমূহ}}:",
        "revdelete-selected-file": "[[:$2]]-এর {{PLURAL:$1|নির্বাচিত ফাইল সংস্করণ|নির্বাচিত ফাইল সংস্করণগুলি}}:",
-       "logdelete-selected": "{{PLURAL:$1|à¦\9fি à¦¨à¦¿à¦°à§\8dবাà¦\9aিত à¦²à¦\97-à¦\98à¦\9fনা|à¦\9fি à¦¨à¦¿à¦°à§\8dবাà¦\9aিত à¦²à¦\97-ঘটনা}}:",
+       "logdelete-selected": "{{PLURAL:$1|নিরà§\8dবাà¦\9aিত à¦²à¦\97à§\87র ঘটনা}}:",
        "revdelete-text-text": "অপসারিত সংস্করণসমূহ এখনও পাতা ইতিহাসে প্রদর্শিত হয়, কিন্তু সেই বিষয়বস্তুর অংশগুলি সর্বসাধারণ দেখতে পারবে না।",
        "revdelete-text-file": "অপসারিত ফাইলের সংস্করণসমূহ এখনও ফাইল ইতিহাসে প্রদর্শিত হয়, কিন্তু সেই বিষয়বস্তুর অংশগুলি সর্বসাধারণ দেখতে পারবে না।",
        "logdelete-text": "অপসারিত লগ ইভেন্টসমূহ এখনও লগে প্রদর্শিত হয়, কিন্তু সেই বিষয়বস্তুর অংশগুলি সর্বসাধারণ দেখতে পারেবে না।",
index c62206b..c7bcb0b 100644 (file)
        "morenotlisted": "Ovaj spisak nije potpun.",
        "mypage": "Stranica",
        "mytalk": "Razgovor",
-       "anontalk": "Razgovor za ovu IP adresu",
+       "anontalk": "Razgovor",
        "navigation": "Navigacija",
        "and": "&#32;i",
        "qbfind": "Pronađite",
        "userlogin-yourname": "Korisničko ime",
        "userlogin-yourname-ph": "Unesite Vaše korisničko ime",
        "createacct-another-username-ph": "Unesite korisničko ime",
-       "yourpassword": "Šifra:",
+       "yourpassword": "Lozinka:",
        "userlogin-yourpassword": "Lozinka",
        "userlogin-yourpassword-ph": "Unesite Vašu lozinku",
        "createacct-yourpassword-ph": "Unesite lozinku",
-       "yourpasswordagain": "Ponovo upišite šifru:",
+       "yourpasswordagain": "Ponovo upišite lozinku:",
        "createacct-yourpasswordagain": "Potvrdite lozinku",
        "createacct-yourpasswordagain-ph": "Unesite lozinku opet",
-       "remembermypassword": "Zapamti moju šifru na ovom pregledniku (najduže $1 {{PLURAL:$1|dan|dana}})",
+       "remembermypassword": "Zapamti moju lozinku na ovom pregledniku (najduže $1 {{PLURAL:$1|dan|dana}})",
        "userlogin-remembermypassword": "Ostavi me prijavljenog/-u",
        "userlogin-signwithsecure": "Koristite sigurnu konekciju",
        "yourdomainname": "Vaš domen:",
        "createacct-emailoptional": "Adresa e-pošte (opcionalno)",
        "createacct-email-ph": "Unesite Vašu adresu e-pоšte",
        "createacct-another-email-ph": "Unesite adresu e-pošte",
-       "createaccountmail": "Koristite privremenu, slučajno stvorenu šifru/lozinku i pošaljite na navedenu adrеsu e-pošte",
+       "createaccountmail": "Koristite privremenu, slučajno stvorenu lozinku i pošaljite na navedenu adrеsu e-pošte",
        "createacct-realname": "Pravo ime (opcionalno)",
        "createaccountreason": "Razlog:",
        "createacct-reason": "Razlog",
        "createacct-reason-ph": "Zašto pravite još jedan korisnički račun?",
        "createacct-submit": "Napravite svoj korisnički račun",
        "createacct-another-submit": "Napravi korisnički račun",
-       "createacct-benefit-heading": "{{SITENAME}} je napravljena od strane ljudi kao što ste Vi.",
+       "createacct-benefit-heading": "{{GRAMMAR:akuzativ|{{SITENAME}}}} stvaraju ljudi poput Vas.",
        "createacct-benefit-body1": "{{PLURAL:$1|izmjena|izmjene}}",
        "createacct-benefit-body2": "{{PLURAL:$1|stranica|stranice|stranica}}",
        "createacct-benefit-body3": "nedavnih {{PLURAL:$1|doprinosa}}",
-       "badretype": "Šifre koje ste unijeli se ne poklapaju.",
+       "badretype": "Lozinke koje ste unijeli se ne poklapaju",
        "usernameinprogress": "Račun za ovo korisničko ime već se pravi. Molimo sačekajte.",
        "userexists": "Korisničko ime je već u upotrebi.\nIzaberite drugo.",
        "loginerror": "Greška pri prijavljivanju",
        "createacct-error": "Došlo je do greške pri otvaranju naloga",
        "createaccounterror": "Ne može se napraviti račun: $1",
-       "nocookiesnew": "Korisnički nalog je napravljen, ali niste prijavljeni.\n{{SITENAME}} koristi kolačiće (cookies) da bi se korisnici prijavili.\nVi ste isključili kolačiće na Vašem računaru.\nMolimo Vas da ih uključite, a onda se prijavite sa svojim novim korisničkim imenom i šifrom.",
+       "nocookiesnew": "Korisnički nalog je napravljen, ali niste prijavljeni.\n{{SITENAME}} koristi kolačiće (cookies) da bi se korisnici prijavili.\nVi ste isključili kolačiće na Vašem računaru.\nMolimo Vas da ih uključite, a onda se prijavite sa svojim novim korisničkim imenom i lozinkom.",
        "nocookieslogin": "{{SITENAME}} koristi kolačiće (''cookies'') da bi se korisnici prijavili.  Vi ste onemogućili kolačiće na Vašem kompjuteru.  Molimo Vas da ih omogućite i da pokušate ponovo sa prijavom.",
        "nocookiesfornew": "Korisnički račun nije napravljen, jer nismo mogli da potvrdimo njegov izvor.\nProvjerite da li su cookies omogućeni, ponovo učitajte ovu stranicu i pokušajte ponovo.",
        "noname": "Niste izabrali ispravno korisničko ime.",
        "nosuchusershort": "Ne postoji korisnik s imenom \"$1\".\nProvjerite jeste li dobro ukucali.",
        "nouserspecified": "Morate izabrati korisničko ime.",
        "login-userblocked": "Ovaj korisnik je blokiran. Prijava nije dopuštena.",
-       "wrongpassword": "Šifra koju ste unijeli je netačna.\nPokušate ponovno.",
-       "wrongpasswordempty": "Šifra koju ste unijeli je bila prazna.\nMolimo Vas da pokušate ponovno.",
-       "passwordtooshort": "Šifra mora imati najmanje {{PLURAL:$1|1 znak|$1 znaka|$1 znakova}}.",
-       "passwordtoolong": "Šifre ne mogu biti duže od {{PLURAL:$1|jednog znaka|$1 znaka|$1 znakova}}.",
+       "wrongpassword": "Lozinka koju ste unijeli je netačna.\nPokušate ponovno.",
+       "wrongpasswordempty": "Lozinka koju ste unijeli je bila prazna.\nMolimo Vas da pokušate ponovno.",
+       "passwordtooshort": "Lozinka mora imati najmanje {{PLURAL:$1|1 znak|$1 znaka|$1 znakova}}.",
+       "passwordtoolong": "Lozinke ne mogu biti duže od {{PLURAL:$1|jednog znaka|$1 znaka|$1 znakova}}.",
        "password-name-match": "Vaša šifra mora biti različita od Vašeg korisničkog imena.",
        "password-login-forbidden": "Korištenje ovih korisničkih imena i šifara je zabranjeo.",
        "mailmypassword": "Poništi lozinku",
        "prefs-watchlist-edits-max": "Najveći broj: 1000",
        "prefs-watchlist-token": "Žeton praćenih članaka:",
        "prefs-misc": "Ostala podešavanja",
-       "prefs-resetpass": "Promijeni šifru",
+       "prefs-resetpass": "Promijeni lozinku",
        "prefs-changeemail": "Promijeni ili ukloni adresu e-pošte",
        "prefs-setemail": "Postavite e-mail adresu",
        "prefs-email": "Opcije e-pošte",
        "contributions": "Doprinosi {{GENDER:$1|korisnika|korisnice|korisnika}}",
        "contributions-title": "Doprinosi korisnika $1",
        "mycontris": "Doprinosi",
+       "anoncontribs": "Doprinosi",
        "contribsub2": "Za {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Korisnički račun \"$1\" nije registrovan.",
        "nocontribs": "Nisu nađene promjene koje zadovoljavaju ove uslove.",
        "sp-contributions-blocked-notice-anon": "Ova IP adresa je trenutno blokirana.\nPosljednje stavke zapisnika blokiranja možete pogledati ispod:",
        "sp-contributions-search": "Pretraži doprinose",
        "sp-contributions-username": "IP adresa ili korisničko ime:",
-       "sp-contributions-toponly": "Prikaži samo izmjene koje su posljednje revizije",
+       "sp-contributions-toponly": "Prikaži samo najnovije izmjene",
        "sp-contributions-newonly": "Prikaži samo izmjene kojima su napravljene nove stranice",
        "sp-contributions-submit": "Traži",
        "whatlinkshere": "Šta vodi ovamo",
        "movenosubpage": "Ova stranica nema podstranica.",
        "movereason": "Razlog:",
        "revertmove": "vrati",
-       "delete_and_move": "Obriši i premjesti",
        "delete_and_move_text": "==Potebno brisanje==\nOdredišna stranica \"[[:$1]]\" već postoji.\nDa li je želite obrisati kako bi ste mogli izvršiti premještanje?",
        "delete_and_move_confirm": "Da, obriši stranicu",
        "delete_and_move_reason": "Obrisano da bi se napravio prostor za premještanje iz \"[[$1]]\"",
index cff76d0..dcb761e 100644 (file)
@@ -52,7 +52,8 @@
                        "Pginer",
                        "Eduardo Martinez",
                        "Matma Rex",
-                       "KRLS"
+                       "KRLS",
+                       "Jaumeortola"
                ]
        },
        "tog-underline": "Subratlla els enllaços:",
        "morenotlisted": "Aquesta llista no és completa.",
        "mypage": "Pàgina",
        "mytalk": "Discussió",
-       "anontalk": "Discussió d'aquesta IP",
+       "anontalk": "Discussió",
        "navigation": "Navegació",
        "and": "&#32;i",
        "qbfind": "Cerca",
        "privacy": "Política de privadesa",
        "privacypage": "Project:Política de privadesa",
        "badaccess": "Error de permisos",
-       "badaccess-group0": "No teniu permís per a executar l'acció que heu soŀlicitat.",
-       "badaccess-groups": "L'acció que heu soŀlicitat es limita als usuaris {{PLURAL:$2|del grup|dels grups}}: $1.",
+       "badaccess-group0": "No teniu permís per a executar l'acció que heu solicitat.",
+       "badaccess-groups": "L'acció que heu solicitat es limita als usuaris {{PLURAL:$2|del grup|dels grups}}: $1.",
        "versionrequired": "Cal la versió $1 del MediaWiki",
        "versionrequiredtext": "Cal la versió $1 del MediaWiki per a utilitzar aquesta pàgina. Vegeu [[Special:Version]]",
        "ok": "D’acord",
        "nstab-category": "Categoria",
        "mainpage-nstab": "Pàgina principal",
        "nosuchaction": "No es reconeix aquesta operació",
-       "nosuchactiontext": "L'acció especificada per la URL no és vàlida.\nPotser heu escrit malament la URL o heu seguit un enllaç incorrecte.\nAixò també pot ser causat per un error al programari utilitzat pel projecte {{SITENAME}}.",
+       "nosuchactiontext": "L'acció especificada per l'URL no és vàlida.\nPotser heu escrit malament l'URL o heu seguit un enllaç incorrecte.\nAixò també pot ser causat per un error en el programari utilitzat pel projecte {{SITENAME}}.",
        "nosuchspecialpage": "No es troba la pàgina especial que busqueu",
        "nospecialpagetext": "<strong>La pàgina especial que demaneu no és vàlida.</strong>\n\nVegeu la llista de pàgines especials a [[Special:SpecialPages]].",
        "error": "Error",
        "userlogin-signwithsecure": "Connexió segura",
        "yourdomainname": "El vostre domini",
        "password-change-forbidden": "No podeu canviar les contrasenyes en aquest wiki.",
-       "externaldberror": "Hi ha hagut una fallida en el servidor d'autenticació externa de la base de dades i no teniu permís per a actualitzar el vostre compte d'accès extern.",
+       "externaldberror": "Hi ha hagut un error en la base de dades d'autenticació o bé no teniu permís per a actualitzar el vostre compte extern.",
        "login": "Inici de sessió",
        "nav-login-createaccount": "Inicia una sessió / crea un compte",
        "userlogin": "Inicia una sessió / crea un compte",
        "wrongpasswordempty": "La contrasenya que s'ha introduït estava en blanc. Torneu-ho a provar.",
        "passwordtooshort": "La contrasenya ha de tenir un mínim {{PLURAL:$1|d'un caràcter|de $1 caràcters}}.",
        "passwordtoolong": "La contrasenya ha de tenir un màxim {{PLURAL:$1|d'un caràcter|de $1 caràcters}}.",
-       "password-name-match": "La contrasenya ha de ser diferent al vostre nom d'usuari.",
+       "password-name-match": "La contrasenya ha de ser diferent del vostre nom d'usuari.",
        "password-login-forbidden": "No és permès d'utilitzar aquest nom d'usuari i contrasenya.",
        "mailmypassword": "Restableix la contrasenya",
        "passwordremindertitle": "Nova contrasenya temporal per al projecte {{SITENAME}}",
-       "passwordremindertext": "Algú (vós mateix segurament, des de l'adreça l'IP $1) ha soŀlicitat que us enviéssim una nova contrasenya per a iniciar la sessió al projecte {{SITENAME}} ($4).\nLa nova contrasenya temporal per a l'usuari «$2» és ara «$3». Si aquesta fou la vostra intenció, ara hauríeu d'iniciar la sessió i canviar-la. Tingueu present que és temporal i caducarà d'aquí {{PLURAL:$5|un dia|$5 dies}}.\n\nSi algú altre hagués fet aquesta soŀlicitud o si ja haguéssiu recordat la vostra contrasenya i\nno volguéssiu canviar-la, ignoreu aquest missatge i continueu utilitzant\nla vostra antiga contrasenya.",
+       "passwordremindertext": "Algú (vós mateix segurament, des de l'adreça l'IP $1) ha sol·licitat que us enviéssim una nova contrasenya per a iniciar la sessió al projecte {{SITENAME}} ($4).\nLa nova contrasenya temporal per a l'usuari «$2» és ara «$3». Si aquesta fou la vostra intenció, ara hauríeu d'iniciar la sessió i canviar-la. Tingueu present que és temporal i caducarà d'aquí {{PLURAL:$5|un dia|$5 dies}}.\n\nSi algú altre hagués fet aquesta sol·licitud o si ja haguéssiu recordat la vostra contrasenya i\nno volguéssiu canviar-la, ignoreu aquest missatge i continueu utilitzant\nla vostra antiga contrasenya.",
        "noemail": "No hi ha cap adreça electrònica registrada de l'usuari «$1».",
        "noemailcreate": "Heu d’indicar una adreça electrònica vàlida.",
        "passwordsent": "S'ha enviat una nova contrasenya a l'adreça electrònica registrada per «$1».\nInicieu una sessió després que la rebeu.",
        "accountcreated": "S'ha creat el compte",
        "accountcreatedtext": "S'ha creat el compte d'usuari de [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|discussió]]).",
        "createaccount-title": "Creació d'un compte a {{SITENAME}}",
-       "createaccount-text": "Algú ha creat un compte d'usuari anomenat $2 al projecte {{SITENAME}}\n($4) amb la vostra adreça de correu electrònic. La contrasenya per a l'usuari «$2» és «$3». Hauríeu d'accedir al compte i canviar-vos aquesta contrasenya quan abans millor.\n\nSi no hi teniu cap relació i aquest compte s'ha creat per error, simplement ignoreu el missatge.",
+       "createaccount-text": "Algú ha creat un compte d'usuari anomenat $2 en el projecte {{SITENAME}}\n($4) amb la vostra adreça de correu electrònic. La contrasenya per a l'usuari «$2» és «$3». Hauríeu d'accedir al compte i canviar-vos aquesta contrasenya com més aviat millor.\n\nSi no hi teniu cap relació i aquest compte s'ha creat per error, simplement ignoreu el missatge.",
        "login-throttled": "Heu realitzat massa intents d'accés a la sessió.\nEspereu $1 abans de tornar-ho a provar.",
-       "login-abort-generic": "L'entrada al compte d'usuari no ha reeixit - Abortada",
+       "login-abort-generic": "L'entrada al compte d'usuari no ha reeixit - S'ha interromput.",
        "login-migrated-generic": "S'ha migrat el vostre compte, i el vostre nom d'usuari ja no existeix en aquest wiki.",
        "loginlanguagelabel": "Llengua: $1",
-       "suspicious-userlogout": "S'ha denegat la vostra petició per tancar la sessió ja què sembla que va ser enviada per un navegador defectuós o un proxy cau.",
+       "suspicious-userlogout": "S'ha denegat la vostra petició per a tancar la sessió, ja que sembla que va ser enviada per un navegador defectuós o un servidor intermediari.",
        "createacct-another-realname-tip": "El nom real és opcional.\nSi decidiu proporcionar-lo, s'utilitzarà per a reconèixer a l'usuari el seu treball.",
        "pt-login": "Inicia la sessió",
        "pt-login-button": "Inicia sessió",
        "resetpass_forbidden": "No poden canviar-se les contrasenyes",
        "resetpass-no-info": "Heu d'estar registrats en un compte per a poder accedir directament a aquesta pàgina.",
        "resetpass-submit-loggedin": "Canvia la contrasenya",
-       "resetpass-submit-cancel": "Canceŀla",
+       "resetpass-submit-cancel": "Cancela",
        "resetpass-wrong-oldpass": "Contrasenya actual o temporal no vàlida.\nDeveu haver canviat la vostra contrasenya o demanat una nova contrasenya temporal.",
        "resetpass-recycled": "Restabliu la contrasenya amb un text diferent que el de la contrasenya actual.",
        "resetpass-temp-emailed": "Heu iniciat una sessió amb un codi temporal enviat per correu.\nPer completar l'inici de sessió heu de definir una contrasenya nova a continuació:",
        "note": "'''Nota:'''",
        "previewnote": "'''Recorda que això és només una previsualització.'''\nEls vostres canvis encara no s'han desat!",
        "continue-editing": "Aneu a l'àrea d'edició",
-       "previewconflict": "Aquesta previsualització reflecteix, a l'àrea\nd'edició superior, el text tal com apareixerà si trieu desar-lo.",
+       "previewconflict": "Aquesta previsualització reflecteix, a l'àrea\nd'edició superior, el text tal com apareixerà si trieu desar-lo.",
        "session_fail_preview": "'''No s'ha pogut processar la vostra modificació a causa d'una pèrdua de dades de la sessió.\nSi us plau, proveu-ho una altra vegada. Si continués sense funcionar, proveu de [[Special:UserLogout|finalitzar la sessió]] i torneu a iniciar-ne una.'''",
        "session_fail_preview_html": "'''Ho sentim, no s'han pogut processar les vostres modificacions a causa d'una pèrdua de dades de la sessió.'''\n\n''Com que el projecte {{SITENAME}} té habilitat l'ús de codi HTML cru, s'ha amagat la previsualització com a prevenció contra atacs mitjançant codis JavaScript.''\n\n'''Si es tracta d'una contribució legítima, si us plau, intenteu-ho una altra vegada. Si continua havent-hi problemes, [[Special:UserLogout|finalitzeu la sessió]] i torneu a iniciar-ne una.'''",
        "token_suffix_mismatch": "'''S'ha rebutjat la vostra modificació perquè el vostre client ha fet malbé els caràcters de puntuació en el testimoni d'edició. S'ha rebutjat la modificació per a evitar la corrupció del text de la pàgina. Açò passa a vegades quan s'utilitza un servei web de servidor intermediari anònim amb problemes.'''",
        "defaultmessagetext": "Missatge per defecte",
        "content-failed-to-parse": "Ha fallat l'anàlisi del contingut de $2 per al model $1: $3",
        "invalid-content-data": "Dades de contingut no vàlides",
-       "content-not-allowed-here": "No Ã©s permés el contingut \"$1\" a la pàgina [[$2]]",
+       "content-not-allowed-here": "No Ã©s permès el contingut \"$1\" a la pàgina [[$2]]",
        "editwarning-warning": "Si sortiu d'aquesta pàgina perdreu tots els canvis que hàgiu fet.\nSi teniu un compte d'usuari, podeu eliminar aquest avís a la secció «{{int:prefs-editing}}» de les vostres preferències.",
        "editpage-notsupportedcontentformat-title": "No s'admet el format del contingut",
        "editpage-notsupportedcontentformat-text": "No s'admet el format del contingut $1 pel model de contingut $2.",
        "parser-unstrip-recursion-limit": "S'ha excedit el límit ($1) de recursivitat no desmuntable",
        "converter-manual-rule-error": "Error detectat a la norma de conversió de llengua manual",
        "undo-success": "Pot desfer-se la modificació. Si us plau, reviseu la comparació de sota per a assegurar-vos que és el que voleu fer; llavors deseu els canvis per a finalitzar la desfeta de l'edició.",
-       "undo-failure": "No pot desfer-se la modificació perquè hi ha edicions entre mig que hi entren en conflicte.",
+       "undo-failure": "No pot desfer-se la modificació perquè hi ha edicions intermèdies en conflicte.",
        "undo-norev": "No s'ha pogut desfer l'edició perquè no existeix o s'ha suprimit.",
        "undo-nochange": "Sembla que ja s'ha desfet la modificació.",
        "undo-summary": "Es desfà la revisió $1 de [[Special:Contributions/$2|$2]] ([[User talk:$2|Discussió]])",
        "undo-summary-username-hidden": "Desfés la revisió $1 d'un usuari ocult",
        "cantcreateaccounttitle": "No es pot crear el compte",
        "cantcreateaccount-text": "[[User:$3|$3]] ha bloquejat la creació de comptes des d'aquesta adreça IP ('''$1''').\n\nEl motiu donat per $3 és ''$2''",
-       "cantcreateaccount-range-text": "La creació de comptes des de les adreces IP en el rang '''$1''', que inclou la vostra adreça IP ('''$4'''), ha esta blocada per [[User:$3|$3]].\n\nEl motiu donat per $3 és ''$2''",
+       "cantcreateaccount-range-text": "La creació de comptes des de les adreces IP en el rang '''$1''', que inclou la vostra adreça IP ('''$4'''), ha estat blocada per [[User:$3|$3]].\n\nEl motiu donat per $3 és ''$2''",
        "viewpagelogs": "Visualitza els registres d'aquesta pàgina",
        "nohistory": "No hi ha un historial de revisions per a aquesta pàgina.",
        "currentrev": "Revisió actual",
        "rev-deleted-no-diff": "No podeu veure aquesta comparativa perquè s'ha '''suprimit''' una de les versions.\nPotser trobareu detalls al [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registre d'esborrats].",
        "rev-suppressed-no-diff": "No podeu veure aquesta diferència perquè s'ha '''suprimit''' una de les revisions.",
        "rev-deleted-unhide-diff": "S'ha '''eliminat''' una de les revisions d'aquesta comparativa.\nVegeu-ne més detalls al [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registre de supressions].\nEncara podeu [$1 veure aquesta comparativa] si així ho desitgeu.",
-       "rev-suppressed-unhide-diff": "S¡ha '''suprimit''' una de les revisions d'aquesta comparativa.\nPodeu veure'n més detalls al [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} registre de supressions].\nPodeu seguir [$1 veient aquesta comparativa] si així ho desitgeu.",
+       "rev-suppressed-unhide-diff": "S'ha <strong>suprimit</strong> una de les revisions d'aquesta comparativa.\nPodeu veure'n més detalls en el [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} registre de supressions].\nEncara podeu [$1 veure aquesta comparativa] si així ho desitgeu.",
        "rev-deleted-diff-view": "S'ha '''suprimit'' una de les revisions d'aquesta comparativa.\nPodeu veure aquesta comparativa; poden haver-hi més detalls al [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registre d'esborraments].",
        "rev-suppressed-diff-view": "S'ha '''suprimit'' una de les revisions d'aquesta comparativa.\nVegeu-ne més detalls al [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} registre de supressions].",
        "rev-delundel": "mostra/amaga",
        "mergehistory-empty": "No pot fusionar-se cap revisió.",
        "mergehistory-done": "{{PLURAL:$3|S’ha|S’han}} fusionat correctament $3 {{PLURAL:$3|revisió|revisions}} de $1 a [[:$2]].",
        "mergehistory-fail": "No s'ha pogut realitzar la fusió de l'historial, comproveu la pàgina i els paràmetres horaris.",
-       "mergehistory-fail-toobig": "No s'ha pogut realitzar la fusió de l'historial perquè es mourien més del limit de $1 {{PLURAL:$1|revisió|revisions}}.",
+       "mergehistory-fail-toobig": "No s'ha pogut fer la fusió de l'historial perquè es mourien més del límit de $1 {{PLURAL:$1|revisió|revisions}}.",
        "mergehistory-no-source": "La pàgina d'origen $1 no existeix.",
        "mergehistory-no-destination": "La pàgina de destinació $1 no existeix.",
        "mergehistory-invalid-source": "La pàgina d'origen ha de tenir un títol vàlid.",
        "right-upload": "Carregar fitxers",
        "right-reupload": "Carregar al damunt d'un fitxer existent",
        "right-reupload-own": "Carregar al damunt d'un fitxer que havia carregat el propi usuari",
-       "right-reupload-shared": "Carregar localment fitxers amb un nom usat en el repostori multimèdia compartit",
+       "right-reupload-shared": "Sobreescriure localment fitxers presents al repositori multimèdia compartit",
        "right-upload_by_url": "Carregar un fitxer des de l'adreça URL",
        "right-purge": "Purgar la memòria cau del lloc web sense pàgina de confirmació",
        "right-autoconfirmed": "Modificar pàgines semiprotegides",
        "action-undelete": "recuperar aquesta pàgina",
        "action-suppressrevision": "revisar i recuperar aquesta revisió oculta",
        "action-suppressionlog": "visualitzar aquest registre privat",
-       "action-block": "blocar aquest usuari per a què no pugui editar",
+       "action-block": "blocar aquest usuari perquè no pugui editar",
        "action-protect": "canviar els nivells de protecció d'aquesta pàgina",
        "action-rollback": "desfer ràpidament les modificacions de l'últim usuari que va editar una determinada pàgina",
        "action-import": "importa pàgines des d'un altre wiki",
        "action-editcontentmodel": "editar el model de contingut d'una pàgina",
        "action-managechangetags": "crear i suprimir etiquetes de la base de dades",
        "action-applychangetags": "aplica les etiquetes juntament amb els canvis",
-       "action-changetags": "afegeix i elimina etiquetes a les revisions y entrades de registre individuals",
+       "action-changetags": "afegeix i elimina etiquetes a les revisions i les entrades de registre individuals",
        "nchanges": "$1 {{PLURAL:$1|canvi|canvis}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|des de la darrera visita}}",
        "enhancedrc-history": "historial",
        "fileexists-thumbnail-yes": "Aquest fitxer sembla ser una imatge en mida reduïda (<em>miniatura</em>). [[$1|thumb]]\nComproveu si us plau el fitxer <strong>[[:$1]]</strong>.\nSi el fitxer és la mateixa imatge a mida original, no cal carregar cap miniatura més.",
        "file-thumbnail-no": "El nom del fitxer comença per <strong>$1</strong>.\nSembla ser una imatge de mida reduïda ''(miniatura)''.\nSi teniu la imatge en resolució completa, pugeu-la, sinó mireu de canviar-li el nom, si us plau.",
        "fileexists-forbidden": "Ja hi existeix un fitxer amb aquest nom i no es pot sobreescriure.\nSi us plau, torneu enrere i carregueu aquest fitxer sota un altre nom. [[File:$1|thumb|center|$1]]",
-       "fileexists-shared-forbidden": "Ja hi ha un fitxer amb aquest nom al fons comú de fitxers.\nSi us plau, si encara desitgeu carregar el vostre fitxer, torneu enrera i carregueu-ne una còpia amb un altre nom. [[File:$1|thumb|center|$1]]",
+       "fileexists-shared-forbidden": "Ja hi ha un fitxer amb aquest nom en el fons comú de fitxers.\nSi encara voleu pujar el fitxer, torneu enrere i pugeu-ne una còpia amb un altre nom. [[File:$1|thumb|center|$1]]",
        "file-exists-duplicate": "Aquest fitxer és un duplicat {{PLURAL:$1|del fitxer |dels següents fitxers:}}",
        "file-deleted-duplicate": "S'ha suprimit anteriorment un fitxer idèntic a aquest ([[:$1]]). Hauríeu de comprovar el registre de supressions del fitxer abans de tornar-lo a carregar.",
        "file-deleted-duplicate-notitle": "Un fitxer idèntic a aquest fitxer havia estat suprimit abans, i també el títol. Hauríeu de demanar a algú que pugui veure les dades suprimides del fitxer que revisi la situació abans de procedir a tornar a carregar-lo.",
        "upload-file-error-text": "S'ha produït un error de càrrega desconegut quan s'intentava crear un fitxer temporal al servidor. Poseu-vos en contacte amb un [[Special:ListUsers/sysop|administrador]].",
        "upload-misc-error": "S'ha produït un error de càrrega desconegut",
        "upload-misc-error-text": "S'ha produït un error desconegut durant la càrrega. Verifiqueu que l'URL és vàlid i accessible, i torneu-ho a provar. Si el problema persisteix, adreceu-vos a un [[Special:ListUsers/sysop|administrador]].",
-       "upload-too-many-redirects": "LURL conté massa redireccions",
+       "upload-too-many-redirects": "L'URL conté massa redireccions",
        "upload-http-error": "Ha ocorregut un error HTTP: $1",
        "upload-copy-upload-invalid-domain": "Les càrregues de còpia no són disponibles des d'aquest domini.",
        "upload-dialog-title": "Carrega un fitxer",
        "backend-fail-stream": "No s'ha pogut transmetre el fitxer $1.",
        "backend-fail-backup": "No s'ha pogut fer una còpia de seguretat del fitxer $1.",
        "backend-fail-notexists": "El fitxer $1 no existeix.",
-       "backend-fail-hashes": "No s'han pogut obtenir els resums dels fitxer per fer-ne comparació.",
+       "backend-fail-hashes": "No s'han pogut obtenir els codis hash dels fitxers per a fer-ne comparació.",
        "backend-fail-notsame": "Ja existeix un fitxer no idèntic a $1.",
        "backend-fail-invalidpath": "$1 no és un camí d'emmagatzemament vàlid.",
        "backend-fail-delete": "No s'ha pogut suprimir el fitxer $1.",
        "img-auth-streaming": "Lectura corrent de \"$1\".",
        "img-auth-public": "La funció de img_auth.php és de sortida de fitxers d'un lloc wiki privat.\nAquest wiki està configurat com a wiki públic.\nPer seguretat, img_auth.php està desactivat.",
        "img-auth-noread": "L'usuari no té accés a la lectura de \"$1\".",
-       "http-invalid-url": "URL incorrecta: $1",
+       "http-invalid-url": "URL incorrecte: $1",
        "http-invalid-scheme": "Les URLs amb l'esquema \"$1\" no són compatibles.",
        "http-request-error": "La petició HTTP ha fallat per un error desconegut.",
        "http-read-error": "Error de lectura HTTP.",
        "filehist-comment": "Comentari",
        "imagelinks": "Ús del fitxer",
        "linkstoimage": "{{PLURAL:$1|La pàgina següent enllaça|Les $1 pàgines següents enllacen}} a aquest fitxer:",
-       "linkstoimage-more": "Hi ha més de $1 {{PLURAL:$1|pàgina que enllaça|pàgines que enllaçen}} a aquest fitxer.\nLa següent llista només mostra {{PLURAL:$1|la primera d'elles|les primeres $1 d'aquestes pàgines}}.\nPodeu consultar la [[Special:WhatLinksHere/$2|llista completa]].",
+       "linkstoimage-more": "Hi ha més de $1 {{PLURAL:$1|pàgina que enllaça|pàgines que enllacen}} a aquest fitxer.\nLa següent llista només mostra {{PLURAL:$1|la primera d'aquestes pàgines|les primeres $1 d'aquestes pàgines}}.\nPodeu consultar la [[Special:WhatLinksHere/$2|llista completa]].",
        "nolinkstoimage": "No hi ha pàgines que enllacin a aquesta imatge.",
        "morelinkstoimage": "Visualitza [[Special:WhatLinksHere/$1|més enllaços]] que porten al fitxer.",
        "linkstoimage-redirect": "$1 (fitxer redirigit) $2",
        "statistics-header-hooks": "Altres estadístiques",
        "statistics-articles": "Pàgines de contingut",
        "statistics-pages": "Pàgines",
-       "statistics-pages-desc": "Totes les pàgines del wiki, incloent les pàgines de discussió, redireccions, etc.",
+       "statistics-pages-desc": "Totes les pàgines del wiki, incloses les pàgines de discussió, redireccions, etc.",
        "statistics-files": "Fitxers carregats",
        "statistics-edits": "Edicions en pàgines des que el projecte {{SITENAME}} fou instal·lat",
        "statistics-edits-average": "Edicions per pàgina de mitjana",
        "pageswithprop-prophidden-long": "valor de propietat text llarg ocult ($1)",
        "pageswithprop-prophidden-binary": "valor de propietat binària oculta ($1)",
        "doubleredirects": "Redireccions dobles",
-       "doubleredirectstext": "Aquesta pàgina llista les pàgines que redirigeixen a altres pàgines de redirecció.\nCada fila conté enllaços a la primera i segona redireccions, així com el destí de la segona redirecció, què generalment és la pàgina destí \"real\", a la què hauria d'apuntar la primera redirecció.\nLes entrades <del>ratllades</del> s'han resolt.",
+       "doubleredirectstext": "Aquesta pàgina llista les pàgines que redirigeixen a altres pàgines de redirecció.\nCada fila conté enllaços a la primera i segona redireccions, així com la destinació de la segona redirecció, que generalment és la pàgina de destinació \"real\" a la qual hauria d'apuntar la primera redirecció.\nLes entrades <del>ratllades</del> s'han resolt.",
        "double-redirect-fixed-move": "S'ha reanomenat [[$1]].\nS'ha actualitzat automàticament i ara redirigeix a [[$2]].",
        "double-redirect-fixed-maintenance": "S'ha arreglat automàticament la redirecció doble de [[$1]] a [[$2]] en un treball de manteniment.",
        "double-redirect-fixer": "Supressor de dobles redireccions",
        "wantedfiles": "Fitxers demanats",
        "wantedfiletext-cat": "Els fitxers següents s'utilitzen per no existeixen. Els fitxers de repositoris aliens poden ser llistats encara que existeixin. Aquells que siguin fals positius es <del>ratllaran</del>. A més, les pàgines que tinguin fitxers incrustats que no existeixin es llistaran a [[:$1]].",
        "wantedfiletext-cat-noforeign": "Els fitxers següents s'utilitzen, però no existeixen. Addicionalment, s'enumeren a [[:$1]] les pàgines que tenen fitxers inserits que no existeixen.",
-       "wantedfiletext-nocat": "Els fitxers següents es fan servir però no existeixen. Els fitxers d'un repositori aliè poden ser llistats encara que existeixin. Tots aquells fals positius es <del>tatxaran</del>.",
+       "wantedfiletext-nocat": "Els fitxers següents es fan servir però no existeixen. Els fitxers d'un repositori aliè poden ser llistats encara que existeixin. Tots aquests falsos positius es <del>ratllaran</del>.",
        "wantedfiletext-nocat-noforeign": "Els fitxers següents s'utilitzen però no existeixen.",
        "wantedtemplates": "Plantilles demanades",
        "mostlinked": "Pàgines més enllaçades",
        "shortpages": "Pàgines curtes",
        "longpages": "Pàgines llargues",
        "deadendpages": "Pàgines atzucac",
-       "deadendpagestext": "Aquestes pàgines no tenen enllaços a d'altres pàgines del projecte {{SITENAME}}.",
+       "deadendpagestext": "Aquestes pàgines no tenen enllaços a altres pàgines del projecte {{SITENAME}}.",
        "protectedpages": "Pàgines protegides",
        "protectedpages-indef": "Només proteccions indefinides",
        "protectedpages-summary": "Aquesta pàgina llista les pàgines existents que estan protegides actualment. Per consultar la llista de títols protegits per tal que no puguin crear-se'n pàgines, vegeu [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "deletereason-dropdown": "*Motius freqüents d'esborrat\n** Brossa\n** Vandalisme\n** Violació del copyright\n** Demanada per l'autor\n** Redirecció trencada",
        "delete-edit-reasonlist": "Edita els motius d'eliminació",
        "delete-toobig": "Aquesta pàgina té un historial d'edicions molt gran, amb més de $1 {{PLURAL:$1|canvi|canvis}}. L'eliminació d'aquestes pàgines està restringida per a prevenir que hi pugui haver un desajustament seriós de la base de dades de tot el projecte {{SITENAME}} per accident.",
-       "delete-warning-toobig": "Aquesta pàgina té un historial d'edicions molt gran, amb més de $1 {{PLURAL:$1|canvi|canvis}}. Eliminar-la podria suposar un seriós desajustament de la base de dades de tot el projecte {{SITENAME}}; aneu en compte abans dur a terme l'acció.",
+       "delete-warning-toobig": "Aquesta pàgina té un historial d'edicions molt gran, amb més de $1 {{PLURAL:$1|canvi|canvis}}. Eliminar-la podria suposar un seriós desajustament de la base de dades de tot el projecte {{SITENAME}}; aneu amb compte abans dur a terme l'acció.",
        "deleteprotected": "No podeu eliminar la pàgina perquè ha estat protegida.",
        "deleting-backlinks-warning": "'''Avís:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|Altres pàgines]] enllacen o transclouen de la pàgina que esteu a punt de suprimir.",
        "rollback": "Reverteix edicions",
        "prot_1movedto2": "[[$1]] mogut a [[$2]]",
        "protect-badnamespace-title": "Espai de nom no-protectable",
        "protect-badnamespace-text": "Les pàgines en aquest espai de nom no pot ser protegit.",
-       "protect-norestrictiontypes-text": "Aquesta pàgina no es pot protegir ja que no hi ha cap tipus de restricció disponible.",
+       "protect-norestrictiontypes-text": "Aquesta pàgina no es pot protegir, ja que no hi ha cap tipus de restricció disponible.",
        "protect-norestrictiontypes-title": "Pàgina no protegible",
        "protect-legend": "Confirmeu la protecció",
        "protectcomment": "Motiu:",
        "mycontris": "Contribucions",
        "contribsub2": "Per a {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "El compte d'usuari «$1» no està registrat.",
-       "nocontribs": "No s’ha trobat cap canvi que encaixessi amb aquests criteris.",
+       "nocontribs": "No s’ha trobat cap canvi que encaixés amb aquests criteris.",
        "uctop": "(actual)",
        "month": "Mes (i anteriors):",
        "year": "Any (i anteriors):",
        "sp-contributions-logs": "registres",
        "sp-contributions-talk": "discussió",
        "sp-contributions-userrights": "gestió de drets d'usuari",
-       "sp-contributions-blocked-notice": "En aquests moments, aquest compte d'usuari es troba blocat.\nPer més detalls, la última entrada del registre es mostra a continuació:",
+       "sp-contributions-blocked-notice": "En aquests moments aquest compte d'usuari està blocat.\nPer a més informació, a continuació es mostra l'última entrada del registre:",
        "sp-contributions-blocked-notice-anon": "En aquests moments, aquesta adreça IP es troba blocada.\nPer més detalls, la última entrada del registre es mostra a continuació:",
        "sp-contributions-search": "Cerca les contribucions",
        "sp-contributions-username": "Adreça IP o nom d'usuari:",
        "autoblockid": "Autoblocatge #$1",
        "block": "Blocatge d'usuaris",
        "unblock": "Desblocatge d'usuaris",
-       "blockip": "Bloca {{GENDER:$1|l'usuari|l'usuària}}",
+       "blockip": "Bloca {{GENDER:$1|l'usuari|lusuària}}",
        "blockip-legend": "Bloca l'usuari",
        "blockiptext": "Empreu el següent formulari per blocar l'accés\nd'escriptura des d'una adreça IP específica o des d'un usuari determinat.\naixò només s'hauria de fer per prevenir el vandalisme, i\nd'acord amb la [[{{MediaWiki:Policy-url}}|política del projecte]].\nEmpleneu el diàleg de sota amb un motiu específic (per exemple, citant\nquines pàgines en concret estan sent vandalitzades).",
        "ipaddressorusername": "Adreça IP o nom de l'usuari",
        "emailblock": "s'ha blocat l'enviament de correus electrònics",
        "blocklist-nousertalk": "no podeu modificar la pàgina de discussió pròpia",
        "ipblocklist-empty": "La llista de bloqueigs està buida.",
-       "ipblocklist-no-results": "L'adreça IP o nom d'usuari soŀlicitat no està bloquejat.",
+       "ipblocklist-no-results": "L'adreça IP o nom d'usuari solicitat no està bloquejat.",
        "blocklink": "bloqueja",
        "unblocklink": "desbloca",
        "change-blocklink": "canvia el blocatge",
        "ipb_cant_unblock": "Errada: No s'ha trobat el núm. ID de bloqueig $1. És possible que ja s'haguera desblocat.",
        "ipb_blocked_as_range": "Error: L'adreça IP $1 no està blocada directament i per tant no pot ésser desbloquejada. Ara bé, sí que ho està per formar part del rang $2 que sí que pot ser desblocat.",
        "ip_range_invalid": "Rang de IP no vàlid.",
-       "ip_range_toolarge": "No estan permesos el bloquejos de rangs més grans que /$1.",
+       "ip_range_toolarge": "No són permesos els bloquejos de rangs més grans que /$1.",
        "proxyblocker": "Bloqueig de proxy",
        "proxyblockreason": "S'ha blocat la vostra adreça IP perquè és un proxy obert. Contactau el vostre proveïdor d'Internet o servei tècnic i informau-los d'aquest seriós problema de seguretat.",
        "sorbsreason": "La vostra adreça IP està llistada com a servidor intermediari (''proxy'') obert dins la llista negra de DNS que fa servir el projecte {{SITENAME}}.",
        "lockdbtext": "Si es bloca la base de dades impedirà la capacitat a tots els usuaris d'editar pàgines, canviar les preferències, editar la llista de seguiment i altres canvis que calen de modificacions a la base de dades.\nConfirmeu que això és el que voleu fer, i sobretot no us oblideu de desblocar la base de dades quan acabeu el manteniment.",
        "unlockdbtext": "Desblocant la base de dades es restaurarà l'habilitat de tots\nels usuaris d'editar pàgines, canviar les preferències, editar els llistats de seguiment, i\naltres accions que requereixen canvis en la base de dades.\nConfirmeu que això és el que voleu fer.",
        "lockconfirm": "Sí, realment vull blocar la base de dades.",
-       "unlockconfirm": "Sí, realment vull desblocar la base dades.",
+       "unlockconfirm": "Sí, realment vull desblocar la base de dades.",
        "lockbtn": "Bloca la base de dades",
        "unlockbtn": "Desbloca la base de dades",
        "locknoconfirm": "No heu respost al diàleg de confirmació.",
        "importuploaderrorsize": "La càrrega del fitxer d'importació ha fallat. El fitxer és més gran que la mida de càrrega permesa.",
        "importuploaderrorpartial": "La càrrega del fitxer d'importació ha fallat. El fitxer s'ha penjat només parcialment.",
        "importuploaderrortemp": "La càrrega del fitxer d'importació ha fallat. Manca una carpeta temporal.",
-       "import-parse-failure": "error en importar l'XML",
+       "import-parse-failure": "error en importar l'XML",
        "import-noarticle": "No hi ha cap pàgina per importar!",
        "import-nonewrevisions": "No s'ha importat cap revisió (ja hi eren abans o s'han omès a causa d'errors).",
        "xml-error-string": "$1 a la línia $2, columna $3 (byte $4): $5",
        "tooltip-pt-mytalk": "La vostra pàgina de discussió.",
        "tooltip-pt-anontalk": "Discussió sobre les edicions per aquesta adreça ip.",
        "tooltip-pt-preferences": "Les vostres preferències.",
-       "tooltip-pt-watchlist": "La llista de pàgines de les que estau vigilant els canvis.",
+       "tooltip-pt-watchlist": "La llista de pàgines de les quals vigileu els canvis.",
        "tooltip-pt-mycontris": "Llista de les vostres contribucions.",
        "tooltip-pt-login": "Us animem a registrar-vos, però no és obligatori",
        "tooltip-pt-logout": "Finalitza la sessió d'usuari",
        "tooltip-ca-protect": "Protegeix aquesta pàgina.",
        "tooltip-ca-unprotect": "Desprotegeix aquesta pàgina",
        "tooltip-ca-delete": "Elimina aquesta pàgina",
-       "tooltip-ca-undelete": "Restaura les edicions fetes a aquesta pàgina abans de que fos esborrada.",
+       "tooltip-ca-undelete": "Restaura les edicions fetes en aquesta pàgina abans que fos esborrada.",
        "tooltip-ca-move": "Reanomena aquesta pàgina",
        "tooltip-ca-watch": "Afegiu aquesta pàgina a la vostra llista de seguiment",
        "tooltip-ca-unwatch": "Suprimiu aquesta pàgina de la vostra llista de seguiment",
        "group-bureaucrat.css": "/* El CSS d'aquí només afectarà els buròcrates */",
        "common.js": "/* Es carregarà per a tots els usuaris, i per a qualsevol pàgina, el codi JavaScript que hi haja després d'aquesta línia. */",
        "group-autoconfirmed.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als usuaris autoconfirmats */",
-       "group-user.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als usuaris rgistrats */",
+       "group-user.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als usuaris registrats */",
        "group-bot.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als bots */",
        "group-sysop.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als sysops */",
        "group-bureaucrat.js": "/* Qualsevol JavaScript d'aquí es carregarà només per als buròcrates */",
        "pageinfo-robot-policy": "Indexació per robots",
        "pageinfo-robot-index": "Permès",
        "pageinfo-robot-noindex": "No permès",
-       "pageinfo-watchers": "Número d'usuaris que vigilen la pàgina",
+       "pageinfo-watchers": "Nombre d'usuaris que vigilen la pàgina",
        "pageinfo-visiting-watchers": "Nombre de vigilants de la pàgina que han visitat els canvis recents",
        "pageinfo-few-watchers": "Menys de $1 {{PLURAL:$1|observador|observadors}}",
        "pageinfo-redirects-name": "Nombre de redireccions a aquesta pàgina",
        "pageinfo-redirects-value": "$1",
        "pageinfo-subpages-name": "Subpàgines d'aquesta pàgina",
-       "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|redirecció|redireccions}}; $3 {{PLURAL:$3|no redireció|no redireccions}})",
+       "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|redirecció|redireccions}}; $3 {{PLURAL:$3|no redirecció|no redireccions}})",
        "pageinfo-firstuser": "Creador de la pàgina",
        "pageinfo-firsttime": "Data de la creació de la pàgina",
        "pageinfo-lastuser": "Últim editor",
        "pageinfo-lasttime": "Data de l'última edició",
-       "pageinfo-edits": "Número total d'edicions",
-       "pageinfo-authors": "Número total d'autors diferents",
-       "pageinfo-recent-edits": "Número d'edicions recents (en els darrers $1)",
-       "pageinfo-recent-authors": "Número recent d'autors diferents",
+       "pageinfo-edits": "Nombre total d'edicions",
+       "pageinfo-authors": "Nombre total d'autors diferents",
+       "pageinfo-recent-edits": "Nombre d'edicions recents (en els darrers $1)",
+       "pageinfo-recent-authors": "Nombre recent d'autors diferents",
        "pageinfo-magic-words": "{{PLURAL:$1|Paraula clau|Paraules clau}} ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1|Categoria oculta|Categories ocultes}} ($1)",
        "pageinfo-templates": "{{PLURAL:$1|plantilla inclosa|plantilles incloses}} ($1)",
        "file-info-png-looped": "embuclat",
        "file-info-png-repeat": "s'ha reproduït $1 {{PLURAL:$1|vegada|vegades}}",
        "file-info-png-frames": "$1 {{PLURAL:$1|fotograma|fotogrames}}",
-       "file-no-thumb-animation": "'''Nota: degut a limitacions tècniques no s'animaran les miniatures per aquest fitxer.'''",
-       "file-no-thumb-animation-gif": "''' Nota: degut a limitacions tècniques no s'animaran les miniatures d'alta resolució d'imatges GIF com aquesta.'''",
+       "file-no-thumb-animation": "<strong>Nota: per limitacions tècniques no s'animaran les miniatures per aquest fitxer.</strong>",
+       "file-no-thumb-animation-gif": "<strong>Nota: per limitacions tècniques no s'animaran les miniatures d'alta resolució d'imatges GIF com aquesta.</strong>",
        "newimages": "Galeria de fitxers nous",
        "imagelisttext": "Llista {{PLURAL:$1|d'un sol fitxer|de '''$1''' fitxers ordenats $2}}.",
        "newimages-summary": "Aquesta pàgina especial mostra els darrers fitxers carregats.",
        "exif-cameraownername": "Propietari de la càmera",
        "exif-label": "Etiqueta",
        "exif-datetimemetadata": "Data que s'ha modificat les metadades per última vegada",
-       "exif-nickname": "Nom informal de l'imatge",
+       "exif-nickname": "Nom informal de limatge",
        "exif-rating": "Valoració (sobre 5)",
        "exif-rightscertificate": "Certificat de gestió de drets",
        "exif-copyrighted": "Estat dels drets d'autor",
        "exif-originaldocumentid": "ID únic del document original",
        "exif-licenseurl": "Direcció de llicències de drets d'autor",
        "exif-morepermissionsurl": "Alternativa informació de llicència",
-       "exif-attributionurl": "Quan re-utilitzis aquest treball, si us plau posa un enllaç a",
-       "exif-preferredattributionname": "Quan re-utilitzis aquest treball, si us plau posa un credit a",
+       "exif-attributionurl": "Quan reutilitzeu aquest treball, si us plau, poseu un enllaç a",
+       "exif-preferredattributionname": "Quan reutilitzeu aquest treball, si us plau posa un credit a",
        "exif-pngfilecomment": "Comentari del fitxer PNG",
        "exif-disclaimer": "Avís general",
        "exif-contentwarning": "Advertència de contingut",
        "exif-orientation-2": "Invertit horitzontalment",
        "exif-orientation-3": "Girat 180°",
        "exif-orientation-4": "Invertit verticalment",
-       "exif-orientation-5": "Rotat 90° en sentit antihorari i invertit verticalment",
-       "exif-orientation-6": "Rotat 90° en sentit antihorari",
-       "exif-orientation-7": "Rotat 90° en sentit horari i invertit verticalment",
-       "exif-orientation-8": "Rotat 90° en sentit horari",
+       "exif-orientation-5": "Girat 90° en sentit antihorari i invertit verticalment",
+       "exif-orientation-6": "Girat 90° en sentit antihorari",
+       "exif-orientation-7": "Girat 90° en sentit horari i invertit verticalment",
+       "exif-orientation-8": "Girat 90° en sentit horari",
        "exif-planarconfiguration-1": "a blocs densos (chunky)",
        "exif-planarconfiguration-2": "format pla",
        "exif-xyresolution-i": "$1 ppp",
        "confirmemail_pending": "Ja s'ha enviat el vostre codi de confirmació per correu electrònic; si\nfa poc hi heu creat el vostre compte, abans de mirar de demanar un nou\ncodi, primer hauríeu d'esperar alguns minuts per a rebre'l.",
        "confirmemail_send": "Envia per correu electrònic un codi de confirmació",
        "confirmemail_sent": "S'ha enviat un missatge de confirmació.",
-       "confirmemail_oncreate": "S'ha enviat un codi de confirmació a la vostra adreça de correu electrònic.\nNo es requereix aquest codi per a autenticar-s'hi, però vos caldrà proporcionar-lo\nabans d'activar qualsevol funcionalitat del wiki basada en missatges\nde correu electrònic.",
+       "confirmemail_oncreate": "S'ha enviat un codi de confirmació a la vostra adreça de correu electrònic.\nAquest codi no cal per a autenticar-se, però haureu de proporcionar-lo abans d'activar qualsevol funcionalitat del wiki basada en missatges de correu electrònic.",
        "confirmemail_sendfailed": "{{SITENAME}} no ha pogut enviar el vostre missatge de confirmació.\nComproveu que l'adreça no tingui caràcters no vàlids.\n\nEl programari de correu retornà el següent missatge: $1",
        "confirmemail_invalid": "El codi de confirmació no és vàlid. Aquest podria haver vençut.",
        "confirmemail_needlogin": "Necessiteu $1 per a confirmar la vostra adreça electrònica.",
        "confirmemail_body": "Algú, segurament vós, ha registrat el compte «$2» al projecte {{SITENAME}}\namb aquesta adreça electrònica des de l'adreça IP $1.\n\nPer a confirmar que aquesta adreça electrònica us pertany realment\ni així activar les opcions de correu del programari, seguiu aquest enllaç:\n\n$3\n\nSi *no* heu estat qui ho ha fet, seguiu aquest altre enllaç per a canceŀlar la confirmació demanada:\n\n$5\n\nAquest codi de confirmació caducarà a $4.",
        "confirmemail_body_changed": "Algú, segurament vós, des de l'adreça IP $1, ha canviat al projecte {{SITENAME}} l'adreça de correu del compte \"$2\" a aquesta adreça.\n\nPer confirmar que aquest compte realment us pertany i per reactivar\nles opcions de correu a {{SITENAME}}, obriu el següent enllaç al vostre navegador:\n\n$3\n\nSi el compte *no* us pertany, seguiu l'enllaç següent\nper a cancel·lar la confirmació d'adreça de correu:\n\n$5\n\nAquest codi de confirmació expirarà el $4.",
        "confirmemail_body_set": "Algú, probablement vós, des de l'adreça IP $1, \nha establert aquesta adreça de correu electrònic com la del compte «$2» del lloc {{SITENAME}}. \n\nPer confirmar que aquest compte realment us pertany i reactivar \nles facilitats de correu electrònic a {{SITENAME}}, cal que obriu al navegador aquest enllaç:\n\n$3\n\nSi el compte *no* us pertany, cancel·leu l'adreça de correu electrònic seguint aquest enllaç: \n\n$5\n\nAquest codi de confirmació caducarà el $4.",
-       "confirmemail_invalidated": "Confirmació d'adreça electrònica canceŀlada",
+       "confirmemail_invalidated": "Confirmació d'adreça electrònica cancelada",
        "invalidateemail": "Canceŀlació d'adreça electrònica",
        "scarytranscludedisabled": "[S'ha inhabilitat la transclusió interwiki]",
        "scarytranscludefailed": "[Ha fallat la recuperació de la plantilla per a $1]",
        "duplicate-displaytitle": "<strong>Avís:</strong> El títol a mostrar «$2» sobreescriu l'anterior títol a mostrar «$1».",
        "invalid-indicator-name": "<strong>Error:</strong> No pot estar buit l'atribut <code>name</code> dels indicadors d'estat de la pàgina.",
        "version": "Versió",
-       "version-extensions": "Extensions instaŀlades",
+       "version-extensions": "Extensions instalades",
        "version-skins": "Temes instal·lats",
        "version-specialpages": "Pàgines especials",
        "version-parserhooks": "Extensions de l'analitzador",
        "tag-filter-submit": "Filtra",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etiqueta|Etiquetes}}]]: $2)",
        "tags-title": "Etiquetes",
-       "tags-intro": "Aquesta pàgina llista les etiquetes amb les què el programari pot marcar una modificació, i llur significat.",
+       "tags-intro": "Aquesta pàgina llista les etiquetes amb què el programari pot marcar una modificació, i el seu significat.",
        "tags-tag": "Nom de l'etiqueta",
        "tags-display-header": "Aparença de la llista de canvis",
        "tags-description-header": "Descripció completa del significat",
        "tags-apply-not-allowed-multi": "No es permet aplicar manualment {{PLURAL:$2|l'etiqueta següent|les etiquetes següents}}: $1",
        "tags-update-no-permission": "No teniu permisos per a afegir o suprimir etiquetes de canvi de revisions individuals o entrades de registre.",
        "tags-update-add-not-allowed-one": "No es permet afegir manualment l'etiqueta «$1».",
-       "tags-update-add-not-allowed-multi": "No es permet afegir manualment {{PLURAL:$2|letiqueta següent|les etiquetes següents}}: $1",
+       "tags-update-add-not-allowed-multi": "No es permet afegir manualment {{PLURAL:$2|l'etiqueta següent|les etiquetes següents}}: $1",
        "tags-update-remove-not-allowed-one": "No es permet treure l’etiqueta «$1».",
-       "tags-update-remove-not-allowed-multi": "No es permet eliminar manualment {{PLURAL:$2|letiqueta següent|les etiquetes següents}}: $1",
+       "tags-update-remove-not-allowed-multi": "No es permet eliminar manualment {{PLURAL:$2|l'etiqueta següent|les etiquetes següents}}: $1",
        "tags-edit-title": "Modifica les etiquetes",
        "tags-edit-manage-link": "Gestiona les etiquetes",
        "tags-edit-revision-selected": "{{PLURAL:$1|Revisió seleccionada|Revisions seleccionades}} de [[:$2]]:",
        "htmlform-select-badoption": "El valor que heu especificat no és una opció vàlida.",
        "htmlform-int-invalid": "El valor que heu especificat no és un nombre enter.",
        "htmlform-float-invalid": "El valor especificat no és un nombre.",
-       "htmlform-int-toolow": "El valor que heu especifcat està per sota del mínim de $1",
+       "htmlform-int-toolow": "El valor que heu especificat està per sota del mínim de $1.",
        "htmlform-int-toohigh": "El valor que heu especificat està per sobre del màxim de $1",
        "htmlform-required": "Aquest valor és necessari",
        "htmlform-submit": "Tramet",
        "htmlform-title-not-exists": "$1 no existeix.",
        "htmlform-user-not-exists": "<strong>$1</strong> no existeix.",
        "htmlform-user-not-valid": "<strong>$1</strong> no és nom d'usuari vàlid.",
-       "sqlite-has-fts": "$1, amb suport de búsqueda de text íntegre",
-       "sqlite-no-fts": "$1, sense supor de búsqueda de text íntegre",
+       "sqlite-has-fts": "$1, amb suport de cerca de text íntegre",
+       "sqlite-no-fts": "$1, sense supor de cerca de text íntegre",
        "logentry-delete-delete": "$1 {{GENDER:$2|ha esborrat}} la pàgina $3",
        "logentry-delete-restore": "$1 ha restaurat $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ha canviat}} la visibilitat {{PLURAL:$5|d'un esdeveniment al registre|de $5 esdeveniments al registre}} de $3: $4",
        "feedback-bugcheck": "Fantàstic! Comproveu que no sigui un dels [$1 problemes ja coneguts].",
        "feedback-bugnew": "Ja ho he comprovat. Informeu d'un nou problema",
        "feedback-bugornote": "Si podeu descriure un problema tècnic en detall, [$1 informeu-ne].\nAltrament, podeu fer servir un senzill formulari a continuació. El vostre comentari s'afegirà a la pàgina «[$3 $2]», juntament amb el vostre nom d'usuari i el navegador que esteu emprant.",
-       "feedback-cancel": "Canceŀla",
+       "feedback-cancel": "Cancela",
        "feedback-close": "Fet",
        "feedback-external-bug-report-button": "Arxiva una tasca tècnica",
        "feedback-dialog-title": "Envia el comentari",
        "api-error-publishfailed": "Error intern: el servidor no ha pogut publicar el fitxer temporal.",
        "api-error-stasherror": "S'ha produït un error en carregar el fitxer al dipòsit.",
        "api-error-stashedfilenotfound": "No s'ha trobat el fitxer de l'espai temporal quan es provava de carregar-lo d'allà.",
-       "api-error-stashpathinvalid": "El camí on s'havia trobar el fitxer de l'espai temporal no és vàlid.",
+       "api-error-stashpathinvalid": "El camí on s'havia de trobar el fitxer de l'espai temporal no és vàlid.",
        "api-error-stashfilestorage": "S'ha produït un error en emmagatzemar el fitxer en l'espai temporal.",
        "api-error-stashzerolength": "El servidor no ha pogut desar el fitxer a l'espai temporal perquè tenia longitud zero.",
        "api-error-stashnotloggedin": "Cal haver iniciat una sessió per desar fitxers en l'espai temporal de càrrega.",
        "limitreport-templateargumentsize": "Mida de l'argument de plantilla",
        "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|byte|bytes}}",
        "limitreport-expansiondepth": "Profunditat màxima d'expansió",
-       "limitreport-expensivefunctioncount": "Número de funcions d'anàlisi dispendioses",
+       "limitreport-expensivefunctioncount": "Nombre de funcions d'anàlisi dispendioses",
        "expandtemplates": "Expansió de plantilles",
        "expand_templates_intro": "Aquesta pàgina especial expandeix de forma recursiva totes les plantilles d'un text donat.\nTambé expandeix les funcions sintàctiques, com ara <code><nowiki>{{</nowiki>#language:…}}</code>, i les variables predefinides, com <code><nowiki>{{</nowiki>CURRENTDAY}}</code> &mdash;de fet, gairebé tot que estigui entre claus dobles.",
        "expand_templates_title": "Títol per contextualitzar ({{FULLPAGENAME}}, etc):",
        "special-characters-group-ipa": "AFI",
        "special-characters-group-symbols": "Símbols",
        "special-characters-group-greek": "Grec",
-       "special-characters-group-cyrillic": "Ciríŀlic",
+       "special-characters-group-cyrillic": "Cirílic",
        "special-characters-group-arabic": "Aràbic",
        "special-characters-group-arabicextended": "Aràbic estès",
        "special-characters-group-persian": "Persa",
index 1bc29ec..f2f5a31 100644 (file)
@@ -50,7 +50,7 @@
        "tog-showhiddencats": "Гайта къайлаха йолу категореш",
        "tog-norollbackdiff": "Юха яьккхиначул тӀаьхьа ма гайта версийн башхо",
        "tog-useeditwarning": "Хаамбе бина хийцамаш дӀаязцабеш аса болх дӀатосучу хенахь",
-       "tog-prefershttps": "Ð\94аима Ð»ÐµÐ»Ð° Ð¹Ðµ Ð»Ð°Ñ\80дина Ñ\81иÑ\81Ñ\82емин Ñ\87Ñ\83далар",
+       "tog-prefershttps": "СиÑ\81Ñ\82емин Ð´Ð¾Ð²Ð·Ð¸Ð¹Ñ\82ина Ñ\87Ñ\83л Ñ\82Ó\80еÑ\85Ñ\8cа Ð´Ð°Ð¸Ð¼Ð°Ð½ Ð»ÐµÐ»Ð°Ð´Ðµ Ð»Ð°Ñ\80дина Ð²Ð¾Ð²Ñ\88аÑ\85Ñ\82аÑ\81ар",
        "underline-always": "Даимна",
        "underline-never": "Цкъа а",
        "underline-default": "Лелае браузеран нисярца",
        "duplicate-args-category": "Кепийн кхайкхамашкахь аргументаш юх-юха лелош йолу агӀонаш",
        "expensive-parserfunction-warning": "'''Тидам бе!''' Ресурсийн функцийн дехарш сов даьлла агӀонаш .\n\nДукху хилла ца деза {{PLURAL:$2|$2 дехар|$2 дехарш|1=цхьана дехар}}, хӀинца $1 {{PLURAL:$1|дехар}} ду.",
        "expensive-parserfunction-category": "Ресурсийн функцийн дехарш сов даьлла агӀонаш",
-       "post-expand-template-inclusion-warning": "Ð\94Ó\80аÑ\85Ñ\8cедаÑ\80: Ñ\8eкÑ\8aа Ñ\82оÑ\8cÑ\85на ÐºÐµÐ¿Ð°Ñ\88ан жамӀан барам тӀех бокха бу. Цхьайолу кепаш юкъа тухур яц.",
-       "post-expand-template-inclusion-category": "ЧÑ\83 Ð´Ñ\83Ñ\8cÑ\85кÑ\83Ñ\88 Ð´Ð¾Ð»Ñ\83 ÐºÐµÐ¿Ð°Ñ\88ан барам тӀех баьлла агӀонаш",
-       "post-expand-template-argument-category": "Ð\9aепаÑ\88ан аргументаш юкъахь йитина агӀонаш",
+       "post-expand-template-inclusion-warning": "Ð\94Ó\80аÑ\85Ñ\8cедаÑ\80: Ñ\8eкÑ\8aа Ñ\82оÑ\8cÑ\85на ÐºÐµÐ¿Ð¸Ð¹н жамӀан барам тӀех бокха бу. Цхьайолу кепаш юкъа тухур яц.",
+       "post-expand-template-inclusion-category": "ЧÑ\83 Ð´Ñ\83Ñ\8cÑ\85кÑ\83Ñ\88 Ð´Ð¾Ð»Ñ\83 ÐºÐµÐ¿Ð¸Ð¹н барам тӀех баьлла агӀонаш",
+       "post-expand-template-argument-category": "Ð\9aепийн аргументаш юкъахь йитина агӀонаш",
        "parser-template-loop-warning": "Карийна кепаш юкъахь хилла шад: [[$1]]",
        "parser-template-recursion-depth-warning": "Дозанал хьаладаьлла кӀоргенца юх юха дина кеп ($1)",
        "node-count-exceeded-category": "Шедийн дукхалла сов даьлла агӀонаш",
index f811fcf..3978408 100644 (file)
        "morenotlisted": "Tento seznam není úplný.",
        "mypage": "Stránka",
        "mytalk": "Diskuse",
-       "anontalk": "Diskuse k této IP adrese",
+       "anontalk": "Diskuse",
        "navigation": "Navigace",
        "and": "&#32;a",
        "qbfind": "Hledání",
        "contributions": "Příspěvky {{GENDER:$1|uživatele|uživatelky}}",
        "contributions-title": "Příspěvky uživatele $1",
        "mycontris": "Příspěvky",
+       "anoncontribs": "Příspěvky",
        "contribsub2": "{{GENDER:$3|uživatele|uživatelky}} $1 ($2)",
        "contributions-userdoesnotexist": "Uživatelský účet „$1“ není zaregistrován.",
        "nocontribs": "Nenalezeny žádné změny vyhovující kritériím.",
        "tooltip-pt-preferences": "Vaše nastavení",
        "tooltip-pt-watchlist": "Seznam stránek, jejichž změny sledujete",
        "tooltip-pt-mycontris": "Seznam vašich příspěvků",
+       "tooltip-pt-anoncontribs": "Seznam editací provedených z této IP adresy",
        "tooltip-pt-login": "Doporučujeme vám přihlásit se, ovšem není to povinné.",
        "tooltip-pt-logout": "Odhlásit se",
        "tooltip-pt-createaccount": "Doporučujeme vytvořit si účet a přihlásit se, není to však povinné",
index aeaa55e..1dad3eb 100644 (file)
        "october-date": "Tışrino Verên $1",
        "november-date": "Tışrino Peyên $1",
        "december-date": "Kanun $1",
-       "pagecategories": "{{PLURAL:$1|Kategori|Kategoriy}}",
+       "pagecategories": "{{PLURAL:$1|Kategoriye|Kategoriyi}}",
        "category_header": "Pelê ke kategoriya \"$1\" tede derê",
        "subcategories": "Kategoriyê bınêni",
        "category-media-header": "Dosyeyê ke kategoriya \"$1\" derê",
        "morenotlisted": "Vêşi lista nêbi...",
        "mypage": "Per",
        "mytalk": "Mesac",
-       "anontalk": "Pela werênayışê nê IPy",
+       "anontalk": "Werênayış",
        "navigation": "Pusula",
        "and": "&#32;u",
        "qbfind": "Bıvêne",
        "history_short": "Tarix",
        "updatedmarker": "cıkewtena mına peyêne ra dıme biyo rocane",
        "printableversion": "Asayışê çapkerdışi",
-       "permalink": "Gıreyo jûqere",
+       "permalink": "Linko daimi",
        "print": "Çap ke",
        "view": "Bıvêne",
        "view-foreign": "$1'i bıvin",
        "randomincategory": "Ğoseri pera kategoriya",
        "randomincategory-invalidcategory": "\"$1\" yew nameyê kategoriya vêrdiye niyo.",
        "randomincategory-nopages": "Kategori da [[:Category:$1|$1]] de qet  per çıniya.",
-       "randomincategory-category": "Kategori:",
+       "randomincategory-category": "Kategoriye:",
        "randomincategory-submit": "Şo",
        "randomredirect": "Serçarnayışo rastameye",
        "randomredirect-nopages": "Cayê nameyê \"$1\" de serşıkıtışi çıniyê.",
        "contributions": "İştıraqê {{GENDER:$1|karber}}i",
        "contributions-title": "Dekerdenê karber de $1",
        "mycontris": "İştıraqi",
+       "anoncontribs": "İştıraqi",
        "contribsub2": "Qandê {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Hesabê karberi \"$1\" qeyd nêbiyo.",
        "nocontribs": "Ena kriteriya de vurnayîş çini yo.",
        "ipb-unblock-addr": "$1 a bik",
        "ipb-unblock": "Yew adresê IPî ya zi nameyê karberî blok bike",
        "ipb-blocklist": "Blokî ke hama estê ey bivîne",
-       "ipb-blocklist-contribs": "Ser $1 îştîrakî",
+       "ipb-blocklist-contribs": "Qandê {{GENDER:$1|}} ra iştıraqi",
        "unblockip": "Hesabê karberî a bike",
        "unblockiptext": "Cıreştışê nuştışê IP ya zi karberio ke ver ra gêriyayo, seba peyser barkerdışi dey rê formê cêrêni bıgurenên.",
        "ipusubmit": "Enê kılitkerdışi wedare",
        "movenotallowedfile": "desturê şıma çino, şıma pelan bıkırışi",
        "cant-move-user-page": "desturê şıma çino, şıma pelanê karberani bıkırışi (bê pelê cerıni).",
        "cant-move-to-user-page": "desturê şıma çino, şıma yew peli bıkırışi pelê yew karberi.",
-       "newtitle": "Nameyê newi:",
+       "newtitle": "Sernameyo newe:",
        "move-watch": "Peler seyr ke",
        "movepagebtn": "Pele bere",
        "pagemovedsub": "Berdışi kerd temam",
        "thumbnail_gd-library": "Configurasyonê katalog ê GDî tam niyo:funksiyonê $1î vînî biyo",
        "thumbnail_image-missing": "Dosya vînî biyo: $1",
        "import": "Peleyi import bik",
-       "importinterwiki": "Împortê transwîkî",
+       "importinterwiki": "Zewbina wiki ra ard",
        "import-interwiki-text": "qey kırıştışê zerreyi yew wiki u pel bıvıcinê.\ntarixê revizyon u nameyê nuştoxi pawyene.\nkarê zerredayişê benateyê wikiyani[[Special:Log/import|zerreyê rocaneyê kırıştî de]] qeyd beno.",
        "import-interwiki-history": "Qeydanê pele pêrune kopya ke",
        "import-interwiki-templates": "Şablonan pêro zerre ke",
        "duplicate-defaultsort": "'''Tembe:''' Hesıbyaye sırmey ratnayış de \"$2\" sırmey ratnayış de \"$1\"i nêhesıbneno.",
        "version": "Versiyon",
        "version-extensions": "Ekstensiyonî ke ronaye",
-       "version-skins": "Cıldi",
+       "version-skins": "Bar kerde bejni",
        "version-specialpages": "Pelanê xasiyan",
        "version-parserhooks": "Çengelê Parserî",
        "version-variables": "Vurnayeyî",
index d4cd76c..bc4207c 100644 (file)
        "revisionasof": "Αναθεώρηση της $1",
        "revision-info": "Αναθεώρηση ως προς $1 από {{GENDER:$6|τον|την}} $2 $7",
        "previousrevision": "← Παλαιότερη αναθεώρηση",
-       "nextrevision": "Î\9dεÏ\8eÏ\84εÏ\81η Î±Î½Î±Î¸ÎµÏ\8eÏ\81ηÏ\83η &rarr;",
+       "nextrevision": "Î\9dεÏ\8cÏ\84εÏ\81η Î±Î½Î±Î¸ÎµÏ\8eÏ\81ηÏ\83η â\86\92",
        "currentrevisionlink": "Τελευταία αναθεώρηση",
        "cur": "τρέχουσα",
        "next": "επόμενη",
        "suppressionlog": "Κατάλογος διαγραφών",
        "suppressionlogtext": "Παρακάτω βρίσκεται μία λίστα με τις διαγραφές και τις φραγές σχετικό  με περιεχόμενο που έχει αποκρυβεί από τους διαχειριστές.\nΔείτε την [[Special:BlockList|λίστα φραγών IP]] για τις τρέχουσες λειτουργικές απαγορεύσεις και φραγές.",
        "mergehistory": "Συγχώνευση ιστορικών σελίδων",
-       "mergehistory-header": "Αυτή η σελίδα σας επιτρέπει να συγχωνεύσετε τις εκδόσεις από το ιστορικό μίας σελίδας πηγής σε μια νεώτερη σελίδα.\nΣιγουρευτείτε ότι αυτή η αλλαγή θα διατηρήσει την συνοχή του ιστορικού της σελίδας.",
+       "mergehistory-header": "Αυτή η σελίδα σας επιτρέπει να συγχωνεύσετε αναθεωρήσεις από το ιστορικό κάποιας σελίδας προέλευσης σε νεότερη σελίδα.\nΣιγουρευτείτε ότι αυτή η αλλαγή θα διατηρήσει τη συνοχή του ιστορικού της σελίδας.",
        "mergehistory-box": "Συγχώνευση εκδόσεων δυο σελίδων:",
        "mergehistory-from": "Σελίδα πηγής:",
        "mergehistory-into": "Σελίδα προορισμού:",
        "notargettext": "Δεν έχετε καθορίσει ένα χρήστη ή μια σελίδα προορισμού για να εκτελεσθεί αυτή η λειτουργία.",
        "nopagetitle": "Δεν υπάρχει τέτοια σελίδα στόχος",
        "nopagetext": "Η σελίδα στόχος που καταχωρίσατε δεν υπάρχει.",
-       "pager-newer-n": "{{PLURAL:$1|1 Î½ÎµÏ\8eÏ\84εÏ\81οÏ\85|$1 Î½ÎµÏ\8eτερων}}",
+       "pager-newer-n": "{{PLURAL:$1|1 Î½ÎµÏ\8cÏ\84εÏ\81οÏ\85|$1 Î½ÎµÏ\8cτερων}}",
        "pager-older-n": "{{PLURAL:$1|1 παλαιότερο|$1 παλαιότερα}}",
        "suppress": "Περιορισμός",
        "querypage-disabled": "Αυτή η ειδική σελίδα είναι απενεργοποιημένη για λόγους απόδοσης.",
        "filedelete-current-unregistered": "Το συγκεκριμένο αρχείο \"$1\" δεν υπάρχει στη βάση δεδομένων.",
        "filedelete-archive-read-only": "Το αρχείο καταλόγου \"$1\" είναι μη εγγράψιμο από τον διακομιστή.",
        "previousdiff": "← Παλαιότερη επεξεργασία",
-       "nextdiff": "Î\9dεÏ\8eτερη επεξεργασία →",
+       "nextdiff": "Î\9dεÏ\8cτερη επεξεργασία →",
        "mediawarning": "'''Προειδοποίηση''': Το αρχείο αυτό μπορεί να περιέχει κακοπροαίρετο κώδικα.\nΕκτελώντας το, μπορεί να βλάψει το σύστημα του υπολογιστή σας.",
        "imagemaxsize": "Όριο μεγέθους εικόνων:<br />''(στις σελίδες περιγραφής εικόνων)''",
        "thumbsize": "Μέγεθος μικρογραφίας:",
index 0092ada..a7f0923 100644 (file)
@@ -35,6 +35,7 @@
        "tog-watchlisthidebots": "Hide bot edits from the watchlist",
        "tog-watchlisthideminor": "Hide minor edits from the watchlist",
        "tog-watchlisthideliu": "Hide edits by logged in users from the watchlist",
+       "tog-watchlistreloadautomatically": "Reload the watchlist automatically whenever a filter is changed (JavaScript required)",
        "tog-watchlisthideanons": "Hide edits by anonymous users from the watchlist",
        "tog-watchlisthidepatrolled": "Hide patrolled edits from the watchlist",
        "tog-watchlisthidecategorization": "Hide categorization of pages",
index 3d34763..bc6230a 100644 (file)
        "morenotlisted": "Ĉi tiu listo ne estas kompleta.",
        "mypage": "Paĝo",
        "mytalk": "Diskuto",
-       "anontalk": "Diskutpaĝo por tiu ĉi IP-adreso",
+       "anontalk": "Diskuto",
        "navigation": "Navigado",
        "and": "&#32;kaj",
        "qbfind": "Trovi",
        "sig_tip": "Via subskribo kun tempindiko",
        "hr_tip": "Horizontala linio (uzu ŝpareme)",
        "summary": "Resumo:",
-       "subject": "Temo/subtitolo:",
+       "subject": "Temo:",
        "minoredit": "Ĉi tiu ŝanĝo estas redakteto",
        "watchthis": "Atenti ĉi tiun paĝon",
        "savearticle": "Konservi ŝanĝojn",
        "columns": "Kolumnoj:",
        "searchresultshead": "Serĉi",
        "stub-threshold": "Ligilformatigo de ĝermoj ($1):",
+       "stub-threshold-sample-link": "specimeno",
        "stub-threshold-disabled": "Malebligita",
        "recentchangesdays": "Tagoj montrendaj en lastaj ŝanĝoj:",
        "recentchangesdays-max": "(maksimume $1 {{PLURAL:$1|tago|tagoj}})",
        "badsig": "Via kaŝnomo (por subskriboj) malvalidas. Bv. kontroli la HTML-etikedojn!",
        "badsiglength": "La subskribo estas tro longa.\nĜi devas esti sub $1 {{PLURAL:$1|signo|signoj}}.",
        "yourgender": "Sekso:",
-       "gender-unknown": "Nespecifita",
+       "gender-unknown": "En mencii vin, la programaro uzos vortojn de neŭtra genro, ĉiam ol estu ebla",
        "gender-male": "Vira",
        "gender-female": "Ina",
        "prefs-help-gender": "Nedeviga: uzita por sekseca salutado de la programaro. Ĉi tiu informo montriĝos publike.",
        "rcshowhidemine": "$1 miajn redaktojn",
        "rcshowhidemine-show": "Montri",
        "rcshowhidemine-hide": "Kaŝi",
+       "rcshowhidecategorization-show": "Montri",
+       "rcshowhidecategorization-hide": "Kaŝi",
        "rclinks": "Montri $1 lastajn ŝanĝojn dum la $2 lastaj tagoj.<br />$3",
        "diff": "malsamoj",
        "hist": "historio",
        "upload-too-many-redirects": "La URL-o enhavis tro multajn alidirektilojn",
        "upload-http-error": "HTTP-eraro okazis: $1",
        "upload-copy-upload-invalid-domain": "Kopio-alŝutoj ne disponiĝas el ĉi tiu domajno.",
+       "upload-dialog-title": "Alŝuti dosieron",
+       "upload-dialog-button-cancel": "Nuligi",
+       "upload-dialog-button-done": "Farite",
+       "upload-dialog-button-save": "Konservi",
+       "upload-dialog-button-upload": "Alŝuti",
+       "upload-form-label-select-file": "Elekti dosieron",
+       "upload-form-label-infoform-title": "Detaloj",
+       "upload-form-label-infoform-name": "Nomo",
+       "upload-form-label-infoform-description": "Priskribo",
+       "upload-form-label-usage-title": "Uzo",
+       "upload-form-label-usage-filename": "Dosiernomo",
+       "foreign-structured-upload-form-label-own-work": "Tio estas mia propra laboro",
+       "foreign-structured-upload-form-label-infoform-categories": "Kategorioj",
+       "foreign-structured-upload-form-label-infoform-date": "Dato",
        "backend-fail-stream": "Ne povis fluigi dosieron $1.",
        "backend-fail-backup": "Ne povis enarkivigi dosieron $1.",
        "backend-fail-notexists": "La dosiero $1 ne ekzistas.",
        "wlnote": "Jen la {{PLURAL:$1|lasta redakto|lastaj <strong>$1</strong> redaktoj}} dum la {{PLURAL:$2|lasta horo|lastaj <strong>$2</strong> horoj}}, ekde $3, $4.",
        "wlshowlast": "Montri el lastaj $1 horoj $2 tagoj",
        "watchlistall2": "ĉiuj",
+       "watchlist-hide": "Kaŝi",
+       "wlshowhideminor": "Etaj redaktoj",
+       "wlshowhidebots": "robotoj",
+       "wlshowhideliu": "registritaj uzantoj",
+       "wlshowhideanons": "anonimaj uzantoj",
+       "wlshowhidepatr": "patrolitaj redaktoj",
+       "wlshowhidemine": "miaj redaktoj",
        "watchlist-options": "Opcioj por atentaro",
        "watching": "Aldonata al la atentaro...",
        "unwatching": "Malatentante...",
index 613a831..8fdedf0 100644 (file)
                        "Tusca",
                        "Tadol",
                        "Nelson6e65",
-                       "Matiia"
+                       "Matiia",
+                       "SinNovedades"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "morenotlisted": "Esta lista no está completa.",
        "mypage": "Página",
        "mytalk": "Discusión",
-       "anontalk": "Discusión para esta IP",
+       "anontalk": "Hablar",
        "navigation": "Navegación",
        "and": "&#32;y",
        "qbfind": "Buscar",
        "contributions": "Contribuciones {{GENDER:$1|del usuario|de la usuaria}}",
        "contributions-title": "Contribuciones {{GENDER:$1|del usuario|de la usuaria}} $1",
        "mycontris": "Contribuciones",
+       "anoncontribs": "Contribuir",
        "contribsub2": "Para {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "La cuenta de usuario «$1» no está registrada.",
        "nocontribs": "No se encontraron cambios que cumplieran estos criterios.",
        "tooltip-pt-preferences": "Tus preferencias",
        "tooltip-pt-watchlist": "Lista de páginas cuyos cambios vigilas",
        "tooltip-pt-mycontris": "Lista de tus contribuciones",
+       "tooltip-pt-anoncontribs": "Una lista de modificaciones hechas desde esta dirección IP",
        "tooltip-pt-login": "Te recomendamos iniciar sesión, aunque no es obligatorio",
        "tooltip-pt-logout": "Salir de la sesión",
        "tooltip-pt-createaccount": "Te recomendamos crear una cuenta e iniciar sesión; sin embargo, no es obligatorio",
index eee23e5..d0854ce 100644 (file)
@@ -22,7 +22,8 @@
                        "Joxemai",
                        "Arkaitz Barnetik",
                        "Sator",
-                       "Macofe"
+                       "Macofe",
+                       "Xð"
                ]
        },
        "tog-underline": "Azpimarratu loturak:",
        "morenotlisted": "Zerrenda hau ez dago osorik.",
        "mypage": "Orrialdea",
        "mytalk": "Eztabaida",
-       "anontalk": "IP honen eztabaida",
+       "anontalk": "Eztabaida",
        "navigation": "Nabigazioa",
        "and": "&#32;eta",
        "qbfind": "Aurkitu",
        "viewhelppage": "Laguntza orrialdea ikusi",
        "categorypage": "Kategoria orrialdea ikusi",
        "viewtalkpage": "Eztabaida ikusi",
-       "otherlanguages": "Erdaretan",
+       "otherlanguages": "Beste hizkuntza batzuetan",
        "redirectedfrom": "($1(e)tik birzuzenduta)",
        "redirectpagesub": "Birbideratze orria",
        "redirectto": "Hona birzuzentzen du:",
        "rev-deleted-user": "(erabiltzailea ezabatu da)",
        "rev-deleted-event": "(log xehetasunak ezabatu dira)",
        "rev-deleted-user-contribs": "[lankide izena edo Ip helbidea ezabatua - aldatu ezkutapena ekarpenetatik]",
-       "rev-deleted-text-permission": "Orrialdearen berrikuspen hau '''ezabatua''' izan da.\nXehetasunak [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ezabaketa erregistroan] ikus daitezke.",
+       "rev-deleted-text-permission": "Orrialdearen berrikuspen hau <strong>ezabatua</strong> izan da.\nXehetasunak [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ezabaketa erregistroan] ikus daitezke.",
        "rev-deleted-text-unhide": "Orriaren bertsio hau '''ezabatu''' da.\nXehetasunak ikusgai daude [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ezabatze erregistroan].\nAdministratzailea zarenez, oraindik [$1 bertsio hau ikus dezakezu], nahi izanez gero.",
        "rev-suppressed-text-unhide": "Orriaren bertsio hau '''ezeztatu''' da.\nXehetasunak ikusgai daude [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} ezeztatze erregistroan].\nAdministratzailea zarenez, oraindik [$1 bertsio hau ikus dezakezu], nahi izanez gero.",
        "rev-deleted-text-view": "Orriaren berrikuspen hau '''ezabatua''' izan da.\nZuk ikusteko aukera daukazu; xehetasunak [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ezabaketa erregistroan] ikus ditzakezu.",
        "rev-showdeleted": "erakutsi",
        "revisiondelete": "Berrikuspenak ezabatu/leheneratu",
        "revdelete-nooldid-title": "Helburu berrikuspenik ez",
-       "revdelete-nooldid-text": "Ez d(it)uzu eragiketa hau burutzeko helburu berrikuspena(k) zehaztu.",
+       "revdelete-nooldid-text": "Ez duzu eragiketa hau burutzeko helburu berrikuspenik zehaztu, edo berrikuspen hori ez da existitzen, edo oraingo berrikuspena ezkutatzen ari zara.",
        "revdelete-no-file": "Zehazturiko fitxategia ez da existitzen.",
        "revdelete-show-file-confirm": "\"<nowiki>$1</nowiki>\" fitxategiaren bertsio ezabatua (eguna: $2; ordua: $3) ikusi nahi duzu?",
        "revdelete-show-file-submit": "Bai",
        "logdelete-selected": "{{PLURAL:$1|Aukeratutako log gertakaria|Aukeratutako log gertakariak}}:",
        "revdelete-confirm": "Baiezta ezazu hori dela zure asmoa, ulertzen dituzula ondorioak, eta [[{{MediaWiki:Policy-url}}|irizpideak]] errespetatuz egiten ari zarela hau.",
-       "revdelete-suppress-text": "Ezabaketa '''bakarrik''' arrazoi hauek direla eta erabili beharko litzateke:\n* Informazio pertsonal desegokia\n*: ''etxeko helbideak eta telefono zenbakiak, segurtasun sozial zenbakiak, etab.''",
+       "revdelete-suppress-text": "Ezabaketa <strong>bakarrik</strong> arrazoi hauek direla eta erabili beharko litzateke:\n* Iraingarria izan daiteken informazioa\n* Informazio pertsonal desegokia\n*: <em>etxeko helbideak eta telefono zenbakiak, nortasun zenbaki nazionalak, etab.</em>",
        "revdelete-legend": "Berrikuspen mugapenak ezarri:",
        "revdelete-hide-text": "Berrikuspenaren testua ezkutatu",
        "revdelete-hide-image": "Fitxategiaren edukia ezkutatu",
        "search-relatedarticle": "Erlazionatua",
        "searchrelated": "erlazionatua",
        "searchall": "guztia",
-       "showingresults": "Jarraian {{PLURAL:$1|emaitza '''1''' ikus daiteke|'''$1''' emaitza ikus daitezke}}, #'''$2'''.etik hasita.",
-       "showingresultsinrange": "Jarraian {{PLURAL:$1|emaitza '''1''' ikus daiteke|'''$1''' emaitza ikus daitezke}}, #'''$2'''.etik hasi eta #<strong>$3</strong>.eraino.",
+       "showingresults": "Jarraian {{PLURAL:$1|emaitza <strong>bat</strong> ageri da.|<strong>$1</strong> emaitza ageri dira, #'''$2'''.etik hasita.}}",
+       "showingresultsinrange": "Jarraian {{PLURAL:$1|emaitza <strong>bat</strong> ageri da.|<strong>$1</strong> emaitza ageri dira, </strong>$2</strong>.etik hasi eta <strong>$3</strong>.eraino.}}",
        "search-showingresults": "{{PLURAL:$4|<strong>$1.</strong> emaitza (guztira <strong>$3</strong> dira)|<strong>$1 - $2.</strong> emaitzak (guztira <strong>$3</strong> dira)}}",
        "search-nonefound": "Ez dago eskaerarekin bat egiten duten emaitzarik.",
        "powersearch-legend": "Bilaketa aurreratua",
        "listgrouprights-addgroup-self-all": "Talde guztiak norbere kontura gehitu",
        "listgrouprights-removegroup-self-all": "Talde guztiak norbere kontutik ezabatu",
        "listgrouprights-namespaceprotection-namespace": "Izen-tartea",
+       "trackingcategories-name": "Mezuaren izena",
        "trackingcategories-nodesc": "Ez dago deskribapenik eskuragarri.",
        "trackingcategories-disabled": "Kategoria desgaitua dago",
        "mailnologin": "Bidalketa helbiderik ez",
        "wlheader-showupdated": "Bisitatu zenituen azken alditik aldaketak izan dituzten orrialdeak '''beltzez''' nabarmenduta daude.",
        "wlnote": "Jarraian {{PLURAL:$2|ikus daiteke azken orduko|ikus daitezke azken '''$2''' orduetako}} azken {{PLURAL:$1|aldaketa|'''$1''' aldaketak}}, $3, $4 gisa.",
        "wlshowlast": "Erakutsi azken $1 orduak, azken $2 egunak",
-       "watchlistall2": "guztiak",
+       "watchlistall2": "guztia",
        "watchlist-hide": "Ezkutatu",
        "wlshowtime": "Erakutsi azkenak:",
        "wlshowhideminor": "aldaketa txikiak",
        "wlshowhidebots": "bot-ak",
+       "wlshowhideliu": "Erregistratutako erabiltzaileak",
        "wlshowhideanons": "erabiltzaile anonimoak",
+       "wlshowhidepatr": "Patruilatutako aldaketak",
        "wlshowhidemine": "nire edizioak",
        "watchlist-options": "Jarraitze-zerrendaren aukerak",
        "watching": "Zerrendan gehitzen...",
        "contributions": "{{GENDER:$1|Lankidearen}} ekarpenak",
        "contributions-title": "$1(r)entzat lankidearen ekarpenak",
        "mycontris": "Ekarpenak",
+       "anoncontribs": "Ekarpenak",
        "contribsub2": "{{GENDER:$3|$1(r)entzat}} ($2)",
+       "contributions-userdoesnotexist": "\"$1\" erabiltzaile-kontua ez dago erregistraturik.",
        "nocontribs": "Ez da ezaugarri horiekin bat datorren aldaketarik aurkitu.",
        "uctop": "(azken aldaketa)",
        "month": "Hilabetea (eta lehenagokoak):",
        "sp-contributions-newbies-sub": "Hasiberrientzako",
        "sp-contributions-newbies-title": "Lankideen ekarpenak lankide berrietn",
        "sp-contributions-blocklog": "Blokeaketa erregistroa",
+       "sp-contributions-suppresslog": "lankide-ekarpen ezabatuak",
        "sp-contributions-deleted": "lankide-ekarpen ezabatuak",
        "sp-contributions-uploads": "igoerak",
        "sp-contributions-logs": "erregistroak",
        "sp-contributions-talk": "eztabaida",
        "sp-contributions-userrights": "erabiltzaile-baimenen kudeaketa",
        "sp-contributions-blocked-notice": "Lankide hau une honetan blokeatuta dago.\nBlokeo erregistroa azken sarrera ematen da azpian erreferentziarako:",
+       "sp-contributions-blocked-notice-anon": "Erabiltzaile hau blokeatuta dago une honetan.\nAzken blokeoaren erregistroa ageri da behean, erreferentzia gisa:",
        "sp-contributions-search": "Ekarpenentzako bilaketa",
        "sp-contributions-username": "IP helbidea edo erabiltzaile izena:",
        "sp-contributions-toponly": "Azken aldaketak direnak soilik erakutsi",
        "unblocked": "[[User:$1|$1]] desblokeatu egin da",
        "unblocked-range": "$1 desblokeatuta izan da.",
        "unblocked-id": "$1 blokeaketa ezabatu da",
+       "unblocked-ip": "[[Special:Contributions/$1|$1]] desblokeatua izan da.",
        "blocklist": "Blokeatutako erabiltzaileak",
        "ipblocklist": "Blokeatutako erabiltzaileak",
        "ipblocklist-legend": "Blokeatutako erabiltzaile bat bilatu",
        "thumbnail_gd-library": "GD liburutegiaren konfigurazio osagabea: $1 funtzioa falta da",
        "thumbnail_image-missing": "Fitxategirik ez dagoela dirudi: $1",
        "import": "Orrialdeak inportatu",
-       "importinterwiki": "Wikien arteko inportazioa",
+       "importinterwiki": "Beste wiki batetik inportatu",
        "import-interwiki-text": "Aukeratu inportatzeko wiki eta orrialde izenburu bat. Berrikuspenen datak eta egileak gorde egingo dira. Inportazio ekintza guzti hauek [[Special:Log/import|inportazio erregistroan]] gordetzen dira.",
        "import-interwiki-sourcewiki": "Jatorrizko wikia:",
        "import-interwiki-sourcepage": "Jatorrizko orria:",
        "tooltip-pt-preferences": "Nire hobespenak",
        "tooltip-pt-watchlist": "Jarraitzen dituzun orrialdeen zerrenda.",
        "tooltip-pt-mycontris": "Nire ekarpenen zerrenda",
+       "tooltip-pt-anoncontribs": "IP helbide honetatik egindako aldaketen zerrenda",
        "tooltip-pt-login": "Izen ematera gonbidatzen zaitugu.",
        "tooltip-pt-logout": "Saioa itxi",
        "tooltip-pt-createaccount": "Kontu bat sortu eta horrekin saioa hastea eskatu nahi genizuke; ez da ezinbestekoa, ordea.",
        "pageinfo-protect-cascading-yes": "Bai",
        "pageinfo-protect-cascading-from": "Serieko babesak aktibatuta. Sorburua:",
        "pageinfo-category-info": "Kategoria informazioa",
+       "pageinfo-category-total": "Erabiltzaile kopurua, guztira",
        "pageinfo-category-pages": "Orrialde kopurua",
        "pageinfo-category-subcats": "Azpikategorien zenbakia",
        "pageinfo-category-files": "Fitxategi kopurua",
        "patrol-log-page": "Patrullatze loga",
        "patrol-log-header": "Hau patruliatutako aldaketen log bat da.",
        "log-show-hide-patrol": "$1 patruilatze loga",
+       "log-show-hide-tag": "$1 etiketa erregistroa",
        "deletedrevision": "$1 berrikuspen zaharra ezabatu da",
        "filedeleteerror-short": "Errorea fitxategia ezabatzerakoan: $1",
        "filedeleteerror-long": "Erroreak gertatu dira fitxategia ezabatzerakoan:\n\n$1",
        "file-info-size-pages": "$1 × $2 pixel, fitxategi tamaina: $3, MIME mota: $4, {{PLURAL:$5|orrialde $5|$5 orrialde}}",
        "file-nohires": "Ez dago bereizmen handiagorik.",
        "svg-long-desc": "SVG fitxategia, nominaldi $1 × $2 pixel, fitxategiaren tamaina: $3",
+       "svg-long-desc-animated": "SVG fitxategi animatua, nominalki $1 × $2 pixel, fitxategiaren tamaina: $3",
        "svg-long-error": "SVG fitxategi ez baliagarria: $1",
        "show-big-image": "Jatorrizko fitxategia",
        "show-big-image-preview": "Aurreikuspen honen neurria: $1.",
        "autosumm-replace": "Orriaren edukiaren ordez, «$1» jarri da",
        "autoredircomment": "[[$1]] orrialdera birzuzentzentzen",
        "autosumm-new": "Orria sortu da. Edukia: $1",
+       "autosumm-newblank": "Orrialde zuria sortu da",
        "lag-warn-normal": "{{PLURAL:$1|segundu $1|$1 segundu}} baino berriagoak diren aldaketak ez dira zerrenda honetan agertuko.",
        "lag-warn-high": "Zerbitzariaren atzerapen handia dela eta, {{PLURAL:$1|segundu $1|$1 segundu}} baino berriagoak diren aldaketak baliteke zerrenda honetan ez azaltzea.",
        "watchlistedit-normal-title": "Jarraitze zerrenda aldatu",
        "watchlistedit-clear-submit": "Garbitu jarraipen zerrenda (Behin betiko da!)",
        "watchlistedit-clear-done": "Zure jarraipen-zerrenda garbitu da.",
        "watchlistedit-clear-removed": "{{PLURAL:$1|Izenburu 1 kendu da|$1 izenburu kendu dira}}:",
+       "watchlistedit-too-many": " Orrialde gehiegi, hemen erakusteko.",
        "watchlisttools-clear": "Garbitu jarraipen-zerrenda",
        "watchlisttools-view": "Aldaketa garrantzitsuak ikusi",
        "watchlisttools-edit": "Zerrenda ikusi eta aldatu",
        "version-ext-colheader-license": "Lizentzia",
        "version-ext-colheader-description": "Deskribapena",
        "version-ext-colheader-credits": "Egileak",
+       "version-license-title": "$1-entzako lizentzia",
+       "version-credits-title": "$1-entzako aitorpena",
        "version-poweredby-credits": "Wiki hau '''[https://www.mediawiki.org/ MediaWiki]'''k sustatzen du (copyright © 2001-$1 $2).",
        "version-poweredby-others": "beste batzuk",
        "version-poweredby-translators": "translatewiki.net itzultzaileak",
        "version-entrypoints": "Sarrera puntuko URLa",
        "version-entrypoints-header-entrypoint": "Sarrera puntua",
        "version-entrypoints-header-url": "URL",
+       "version-libraries": "Instalatutako bibliotekak",
        "version-libraries-library": "Liburutegia",
        "version-libraries-version": "Bertsioa",
        "version-libraries-license": "Lizentzia",
        "tags-actions-header": "Ekintzak",
        "tags-active-yes": "Bai",
        "tags-active-no": "Ez",
+       "tags-source-extension": "Luzapenak definitua",
+       "tags-source-none": "Ez da gehiago erabiltzen",
        "tags-edit": "aldatu",
        "tags-delete": "ezabatu",
        "tags-activate": "aktibatu",
        "tags-deactivate": "desaktibatu",
        "tags-hitcount": "$1 {{PLURAL:$1|aldaketa|aldaketa}}",
+       "tags-manage-no-permission": "Ez duzu etiketa aldaketak kudeatzeko baimenik.",
        "tags-create-heading": "Etiketa berria sortu",
        "tags-create-tag-name": "Etiketaren izena:",
        "tags-create-reason": "Arrazoia:",
        "tags-create-submit": "Sortu",
+       "tags-create-already-exists": "\"$1\" etiketa badago.",
+       "tags-create-warnings-below": "Etiketaren sorrerarekin jarraitu nahi duzu?",
        "tags-delete-title": "Etiketa ezabatu",
+       "tags-delete-explanation-initial": "Datu-basetik \"$1\" etiketa ezabatzera zoaz",
        "tags-delete-reason": "Arrazoia:",
        "tags-delete-not-found": "\"$1\" etiketa  ez da existitzen.",
        "tags-activate-title": "Etiketa aktibatu",
        "compare-revision-not-exists": "Zehazturiko berrikuspena ez da existitzen.",
        "dberr-problems": "Barkatu! Webgune honek zailtasun teknikoak jasaten ari da.",
        "dberr-again": "Saiatu pare bat minutu itxaroten edo kargatu ezazu orrialdea berriro.",
-       "dberr-info": "($1: Ezin da datu-base zerbitzariarekin konektatu)",
-       "dberr-info-hidden": "(Ezin da konektatu datubasearen zerbitzariarekin)",
+       "dberr-info": "($1: Ezin da datu-basera konektatu)",
+       "dberr-info-hidden": "(Ezin da konektatu datu-basera)",
        "dberr-usegoogle": "Bitartean Google bidez bilatzen saiatu zintezke.",
        "dberr-outofdate": "Eduki hauek aurkibideak eguneratu gabe egon daitezke.",
        "dberr-cachederror": "Ondorengoa eskatutako orriaren katxedun kopia da, eta eguneratu gabe egon daiteke.",
        "logentry-newusers-create": "$1 erabiltzaile kontua {{GENDER:$2|sortu da}}",
        "logentry-newusers-create2": "$1 wikilariak $3 erabiltzaile kontua sortu du",
        "logentry-upload-upload": "$1(e)k $3 {{GENDER:$2|igo du}}",
+       "log-name-tag": "Etiketen erregistroa",
        "rightsnone": "(bat ere ez)",
        "revdelete-summary": "aldaketaren laburpena",
        "feedback-adding": "Orriari feedbacka gehitzen...",
        "feedback-bugnew": "Txekeatu dut. Bug berria bidaliko",
        "feedback-cancel": "Utzi",
        "feedback-close": "Egina",
+       "feedback-dialog-title": "Feedbacka bidali",
        "feedback-error-title": "Errorea",
        "feedback-error1": "Akatsa: APIaren emaitza ez ezagunak",
        "feedback-error2": "Akatsa: Aldaketa ez da egin",
        "expand_templates_remove_comments": "Iruzkinak kendu",
        "expand_templates_remove_nowiki": "Ezabatu <nowiki> etiketen emaitzak",
        "expand_templates_generate_xml": "Erakutsi XML parse zuhaitza",
+       "expand_templates_generate_rawhtml": "Erakutsi HTML gordina",
        "expand_templates_preview": "Aurreikusi",
        "pagelanguage": "Orriaren hizkuntza aukeratu",
        "pagelang-name": "Orria",
index 07a303a..ced2458 100644 (file)
        "morenotlisted": "Cette liste n’est pas complète.",
        "mypage": "Page",
        "mytalk": "Discussion",
-       "anontalk": "Discussion avec cette adresse IP",
+       "anontalk": "Discussion",
        "navigation": "Navigation",
        "and": "&#32;et",
        "qbfind": "Rechercher",
        "contributions": "Contributions de l’{{GENDER:$1|utilisateur|utilisatrice}}",
        "contributions-title": "Liste des contributions de l’utilisat{{GENDER:$1|eur|rice|eur}} $1",
        "mycontris": "Contributions",
+       "anoncontribs": "Contributions",
        "contribsub2": "Pour {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Le compte utilisateur « $1 » n’est pas enregistré.",
        "nocontribs": "Aucune modification correspondant à ces critères n'a été trouvée.",
        "tooltip-pt-preferences": "Vos préférences",
        "tooltip-pt-watchlist": "La liste des pages dont vous suivez les modifications",
        "tooltip-pt-mycontris": "La liste de vos contributions",
+       "tooltip-pt-anoncontribs": "Une liste des modifications effectuées depuis cette adresse IP",
        "tooltip-pt-login": "Il est recommandé de vous identifier ; ce n'est cependant pas obligatoire.",
        "tooltip-pt-logout": "Se déconnecter",
        "tooltip-pt-createaccount": "Il vous est conseillé de créer un compte et de vous connecter ; cependant, ce n’est pas obligatoire",
index cf5c59d..bf2acf4 100644 (file)
        "morenotlisted": "Esta lista non está completa.",
        "mypage": "Páxina",
        "mytalk": "Conversa",
-       "anontalk": "Conversa con este enderezo IP",
+       "anontalk": "Conversa",
        "navigation": "Navegación",
        "and": "&#32;e",
        "qbfind": "Procurar",
        "contributions": "Contribucións {{GENDER:$1|do usuario|da usuaria}}",
        "contributions-title": "Contribucións de $1",
        "mycontris": "Contribucións",
+       "anoncontribs": "Contribucións",
        "contribsub2": "De {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "A conta de usuario \"$1\" non está rexistrada.",
        "nocontribs": "Non se deron atopado cambios con eses criterios.",
        "tooltip-pt-preferences": "As miñas preferencias",
        "tooltip-pt-watchlist": "A lista de páxinas cuxas modificacións está a seguir",
        "tooltip-pt-mycontris": "Lista das súas contribucións",
+       "tooltip-pt-anoncontribs": "Unha lista de modificacións feitas desde esta dirección IP",
        "tooltip-pt-login": "Recoméndaselle rexistrarse, se ben non é obrigatorio",
        "tooltip-pt-logout": "Saír ao anonimato",
        "tooltip-pt-createaccount": "Recoméndaselle crear unha conta e acceder ao sistema, se ben non é obrigatorio",
index 845482c..e5edb1c 100644 (file)
        "wlnote": "Ovdje {{PLURAL:$1|je posljednja $1 promjena|su posljednje $1 promjene|je posljednjih $1 promjena}} u {{PLURAL:$2|posljednjem <strong>$2</strong> satu|posljednja '''$2''' sata|posljednjih <strong>$2</strong> sati}}, od $3, $4.",
        "wlshowlast": "Prikaži posljednjih $1 sati $2 dana",
        "watchlistall2": "sve",
+       "wlshowtime": "Prikaži posljednjih:",
+       "wlshowhideminor": "manje promjene",
+       "wlshowhidebots": "botove",
+       "wlshowhideliu": "prijavljene suradnike",
+       "wlshowhideanons": "neprijavljene suradnike",
+       "wlshowhidemine": "moje promjene",
        "watchlist-options": "Izbornik popisa praćenja",
        "watching": "Pratim...",
        "unwatching": "Prestajem pratiti...",
index ef6cdfa..0c35967 100644 (file)
        "shown-title": "$1 {{PLURAL:$1|wuslědk|wuslědkaj|wuslědki|wuslědkow}} na stronu pokazać",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) pokazać",
        "searchmenu-exists": "'''Je strona z mjenom \"[[$1]]\" na tutym wikiju'''",
-       "searchmenu-new": "<strong>Wutwor stronu \"[[:$1]]\" na tutym wikiju!</strong> {{PLURAL:$2|0=|Hlej tež stronu namakanu z twojim pytanjom.|Hlej tež namakane pytanske wuslědki.}}",
+       "searchmenu-new": "<strong>Wutwor stronu \"[[:$1]]\" na tutym wikiju!</strong> {{PLURAL:$2|0=|Hlej tež stronu namakanu z twojim pytanjom.|Dźiwaj tež na namakane pytanske wuslědki.}}",
        "searchprofile-articles": "Wobsahowe strony",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Wšitko",
        "movenosubpage": "Tuta strona podstrony nima.",
        "movereason": "Přičina:",
        "revertmove": "wróćo přesunyć",
-       "delete_and_move": "wušmórnyć a přesunyć",
        "delete_and_move_text": "== Wušmórnjenje trěbne ==\n\nCilowa strona „[[:$1]]” hižo eksistuje. Chceš ju wušmórnyć, zo by so přesunjenje zmóžniło?",
        "delete_and_move_confirm": "Haj, stronu wušmórnyć.",
        "delete_and_move_reason": "Wušmórnjena, zo by so rum za přesunjenje z \"[[$1]]\" wutworił.",
index a147dc9..54d78c3 100644 (file)
        "morenotlisted": "Daytoy a listaan ket saan a kompleto.",
        "mypage": "Panid",
        "mytalk": "Tungtungan",
-       "anontalk": "Tungtungan para iti daytoy a pagtaengan ti IP",
+       "anontalk": "Tungtungan",
        "navigation": "Pagdaliasatan",
        "and": "&#32;ken",
        "qbfind": "Biruken",
        "viewsource": "Kitaen ti taudan",
        "viewsource-title": "Kitaen ti taudan para iti $1",
        "actionthrottled": "Napabuntog ti aramid",
-       "actionthrottledtext": "Para iti pagkontra ti spam, naipatinggaka nga agramid iti daytoy a tignay iti adu unay a beses iti nasiket nga oras, ken nalabsamon daytoy a patingga.\nPangngaasi nga ipadasmo manen no madamdama.",
+       "actionthrottledtext": "Kas pangkontra iti spam, naipatinggaka nga agaramid iti daytoy a tignay iti adu unay a beses iti nabiit a panawen, ken nalabsamon daytoy a patingga.\nPangngaasi nga ipadasmo manen intono madamdama.",
        "protectedpagetext": "Nasalakniban daytoy a panid tapno mapawilan ti panagurnos wenno dagiti dadduma pay a tignay.",
        "viewsourcetext": "Mabalinmo a kitaen ken tuladen ti taudan daytoy a panid.",
        "viewyourtext": "Mabalinmo a makita ken tuladen ti taudan dagiti <strong>inurnosmo</strong> iti daytoy panid.",
        "passwordreset-emailtext-ip": "Adda (baka sika, ti naggapuan ti IP a pagtaengan $1) a nagkiddaw ti maysa a panangisaad manen ti kontrasenias para iti {{SITNAME}} ($4) . {{PLURAL:$3|Ti |Dagiti}} sumaganad a pakabilangan ti agar-aramat ket\nmainaig iti daytoy nga esurat a pagtaengan:\n\n$2\n\n{{PLURAL:$3|Daytoy temporario a kontrasenias|Dagitoy temporario a kontrasenias}} ket agpaso {{PLURAL:$5|iti maysa nga aldaw|kadagiti $5 nga aldaw}}.\nSumrekka koman tapno agpilika ti baro a kontraseniasmo tattan. No adda met sabali a nagaramid daytoy a \npanagkiddaw, wenno malagipmo ti dati a kontraseniasmo, ket saanmo a kayaten a sukatan, saanmo nga ikaskaso daytoy a mensahe ken \nagtuloyka nga agusar ti daan a kontrasenias.",
        "passwordreset-emailtext-user": "Daytoy nga agar-aramat $1 iti {{SITENAME}} ket nagkiddaw ti maysa a panangisaad manen ti bukod a kontrasenias para iti {{SITENAME}}\n($4) . {{PLURAL:$3|Ti|Dagiti}} sumaganad a pakabilangan ti agar-aramat ket\nmainaig iti daytoy nga esurat a pagtaengan:\n\n$2\n\n{{PLURAL:$3|Daytoy temporario a kontrasenias|Dagitoy temporario a kontrasenias}} ket agpaso {{PLURAL:$5|iti maysa nga aldaw|kadagiti $5 nga aldaw}}.\nSumrekka koman tapno agpilika ti baro a kontraseniasmo tattan. No adda met sabali a nagaramid daytoy a \npanagkiddaw, wenno malagipmo ti dati a kontraseniasmo, ken saanmo a kayaten a sukatan, saanmo nga ikaskaso daytoy a mensahe ken \nagtuloykan nga agusar ti daan a kontraseniasmo.",
        "passwordreset-emailelement": "Nagan ti agar-aramat: \n$1\n\nTemporario a kontrasenias: \n$2",
-       "passwordreset-emailsent": "Ti maysa nga esurat ti panangisaad manen ti kontrasenias ket naipatuloden.",
+       "passwordreset-emailsent": "No daytoy ket nairehistro nga adres ti esurat para iti pakabilangam, maipatulodto ti maysa nga esurat iti panangisaad manen ti kontrasenias.",
        "passwordreset-emailsent-capture": "Ti maysa nga esurat ti panangisaad manen ti kontrasenias ket naipatuloden, a naipakita dita baba.",
        "passwordreset-emailerror-capture": "Naaramid ti maysa nga esurat a panangisaad manen ti kontrasenias, a napaikita dita baba, ngem ti panangitulod kenni {{GENDER:$2|agar-aramat}} ket napaay: $1",
-       "changeemail": "Sukatan ti esurat a pagtaengan",
-       "changeemail-header": "Sukatan ti esurat a pagtaengan ti pakabilangan",
+       "changeemail": "Sukatan wenno ikkaten ti adres ti esurat",
+       "changeemail-header": "Kompletuen daytoy a porma tapno masukatan ti adres ti esuratmo. No kayatmo a maikkat ti pannakainaig iti ania man nga adres ti esurat manipud iti pakabilangam, ibati a blanko ti baro nga adres ti esurat no ited ti porma.",
+       "changeemail-passwordrequired": "Masapulmonto nga ikabil ti kontraseniasmo tapno mapasingkedan daytoy a panagbaliw.",
        "changeemail-no-info": "Masapul a nakastrekka tapno dagus a makapan iti ditoy a panid.",
        "changeemail-oldemail": "Agdama nga esurat a pagtaengan:",
        "changeemail-newemail": "Baro nga esurat a pagtaengan:",
+       "changeemail-newemail-help": "Daytoy a pagikabilan ket nasken a blanko no kayatmo a maikkat ti adres ti esuratmo. Saanmonto a mabalin ti mangisaad manen ti nalipatan a kontrasenias ken saankanto a makaawat kadagiti esurat manipud iti daytoy a wiki no maikkat ti adres ti esurat.",
        "changeemail-none": "(awan)",
        "changeemail-password": "Ti bukodmo a kontrasenias ti {{SITENAME}}:",
        "changeemail-submit": "Sukatan ti esurat",
        "sig_tip": "Ti pirmam nga addaan iti oras ken petsa",
        "hr_tip": "Horisontal a linia (manmano laeng nga aramaten)",
        "summary": "Pakabuklan:",
-       "subject": "Suheto/paulo:",
+       "subject": "Suheto:",
        "minoredit": "Daytoy ket bassit a panagurnos",
        "watchthis": "Bantayan daytoy a panid",
        "savearticle": "Idulin ti panid",
        "missingsummary": "<strong>Palagip:</strong> Saanka a nakaited iti pakabuklan ti panagurnos.\nNo pindutem manen ti \"{{int:savearticle}}\", maidulin ti inurnosmo nga awan ti pakabuklanna.",
        "selfredirect": "<strong>Ballaag:</strong> Ibawbaw-ingmo daytoy a panid iti isu met laeng a panid.\nMabalinmo nga innaganan ti kamali a puntaan para iti baw-ing, wenno mabalin nga ur-urnosem ti kamali a panid.\nNo pindutem manen ti \"{{int:savearticle}}\" , mapartuatto lattan ti baw-ing.",
        "missingcommenttext": "Pangngaasi nga agikabil ti komentario dita baba.",
-       "missingcommentheader": "<strong>Palagip:</strong> Saanka a nakaited iti suheto/paulo para iti daytoy a komentario.\nNo pindutem manen ti \"{{int:savearticle}}\", maidulin ti inurnosmo nga awan ti pakabuklanna.",
+       "missingcommentheader": "<strong>Palagip:</strong> Saanka pay a nakaited iti suheto para iti daytoy a komentario.\nNo pindutem manen ti \"{{int:savearticle}}\", maidulinto ti inurnosmo nga awan ti pakabuklanna.",
        "summary-preview": "Naipadas a pakabuklan:",
-       "subject-preview": "Suheto/naipadas a paulo:",
+       "subject-preview": "Naipadas a suheto:",
        "previewerrortext": "Adda napasamak a maysa a biddut bayat a nagpadpadas kadagiti binawbaliwam.",
        "blockedtitle": "Naseraan ti agar-aramat",
        "blockedtext": "<strong>Naseraan ti naganmo nga agar-aramat wenno ti IP a pagtaengam.</strong>\n\nTi serra ket inaramid babaen ni $1. \nTi rason a naited ket <em>$2</em>.\n\n* Rugi ti serra: $8\n* Panagpaso ti serra: $6\n* Naikeddeng a serraanna: $7\n\nMabalinmo a kontaken ni $1 wenno sabali pay nga [[{{MediaWiki:Grouppage-sysop}}|administrador]] no kayatmo a maipalawag daytoy a panagserra.\nDimo mabalin nga aramaten ti ramit nga esuratan daytoy nga agar-aramat malaksid no adda napudno nga esurat a pagtaengan a nainaganan iti [[Special:Preferences|pakabilangan ti kakaykayatm]] ken no saanka a naparitan nga agaramat iti daytoy.\nTi agdama nga IP a pagtaengam ket $3, ti naserraan nga ID ket #$5. \nPangngaasi nga iramanmo amin dagiti salaysay dita ngato kadagiti ania man nga aramidem nga usisa.",
        "permissionserrors": "Biddut ti pammalubos",
        "permissionserrorstext": "Awan ti pammalubosmo nga agaramid iti dayta, gapu ti sumaganad {{PLURAL:$1|a rason|a rasrason}}:",
        "permissionserrorstext-withaction": "Awan ti pammalubosmo nga $2, gapu ti sumaganad a {{PLURAL:$1|rason|rasrason}}:",
+       "contentmodelediterror": "Saanmo a maurnos daytoy a rebision gapu ta ti modelo ti linaon ket <code>$1</code>, ken ti agdama a linaon ti panid ket <code>$2</code>.",
        "recreate-moveddeleted-warn": "<strong>Ballaag: Agparpartuatka manen ti dati a naikkat a panid.</strong>\n\nUsigem koma no maitutop ti agtuloy nga agurnos iti daytoy a panid.\nTi listaan ti pannakaikkat ken pannakaiyalis para iti daytoy a panid ket naited ditoy para iti pakainugotan:",
        "moveddeleted-notice": "Naikkaten daytoy a panid.\nTi listaan ti pannakaikkat ken pannakaiyalis para iti panid ket naited dita baba para iti reperensia.",
        "moveddeleted-notice-recent": "Pasensian, daytoy a panid ket kaik-ikkat idi (iti kaunegan dagiti 24 nga oras).\nTi listaan ti pannakaikkat ken pannakaiyalis para iti panid ket naited dita baba para iti reperensia.",
        "mergehistory-go": "Ipakita dagiti mabalin a maitipon a panagurnos",
        "mergehistory-submit": "Pagtitiponen dagiti rebision",
        "mergehistory-empty": "Awan dagiti rebision ti mabalin nga itipon.",
-       "mergehistory-done": "$3 {{PLURAL:$3|a rebision|dagiti rebision}} iti $1 ket nagballigi a naitipon iti [[:$2]].",
+       "mergehistory-done": "$3 {{PLURAL:$3|a rebision|dagiti rebision}} iti $1 ket {{PLURAL:$3|naitipon|naitiponda}} iti [[:$2]].",
        "mergehistory-fail": "Saan a nakaaramid ti panagtipon ti pakasaritaan, pangngaasi a kitaen ti panid ken dagiti parametro ti oras.",
        "mergehistory-fail-toobig": "Di naaramid ti panagtipon ti pakasaritaan gapu ta ad-adu ti patingga ti $1 {{PLURAL:$1|a rebision|kadagiti rebision}} ti maiyalisto.",
        "mergehistory-no-source": "Awan ti taudan ti panid ti $1.",
        "showingresultsinrange": "Mangipakpakita aginggana {{PLURAL:$1|iti <strong>1</strong> a resulta|dagiti <strong>$1</strong> a resulta}} iti sakop ti #<strong>$2</strong> aginggana iti #<strong>$3</strong>.",
        "search-showingresults": "{{PLURAL:$4|Nagbanagan a <strong>$1</strong> iti <strong>$3</strong>|Dagiti nagbanagan a <strong>$1 - $2</strong> iti <strong>$3</strong>}}",
        "search-nonefound": "Awan dagiti nagbanagan a maipada iti usisa.",
+       "search-nonefound-thiswiki": "Awan dagiti resulta a maipada iti panagusisa iti daytoy a sitio.",
        "powersearch-legend": "Napasayat a panagbiruk",
        "powersearch-ns": "Agbiruk kadagiti nagan ti espasio:",
        "powersearch-togglelabel": "Markaan:",
        "prefs-watchlist-token": "Tandaan ti listaan ti bambantayan:",
        "prefs-misc": "Sabsabali",
        "prefs-resetpass": "Sukatan ti kontrasenias",
-       "prefs-changeemail": "Sukatan ti esurat a pagtaengan",
+       "prefs-changeemail": "Sukatan wenno ikkaten ti adres ti esurat",
        "prefs-setemail": "Isaad ti esurat a pagtaengan",
        "prefs-email": "Dagiti pagpilian ti esurat",
        "prefs-rendering": "Tabas",
        "prefs-help-recentchangescount": "Daytoy ket mangiraman iti kaudian a balbaliw, dagiti pakasaritaan ti panid, ken dagiti listaan.",
        "prefs-help-watchlist-token2": "Daytoy ti sekreto a tulbek iti pakan ti web iti listaan ti bambantayam.\nTi sinoman a makaammo daytoy ket mabalinda a basaen ti listaan ti bambantayam, isu a saanmo nga ipabingay.\nNo masapulmo, [[Special:ResetTokens|mabalinmo nga isaad manen]].",
        "savedprefs": "Naidulinen dagiti kakaykayatam.",
+       "savedrights": "Naidulinen dagiti karbengan ti agar-aramat ni {{GENDER:$1|$1}}.",
        "timezonelegend": "Sona ti oras:",
        "localtime": "Lokal nga oras:",
        "timezoneuseserverdefault": "Usaren ti kasisigud ti wiki ($1)",
        "recentchanges-page-added-to-category-bundled": "nainayon ti [[:$1]] ken {{PLURAL:$2|maysa a panid|$2 a pampanid}} iti kategoria",
        "recentchanges-page-removed-from-category": "naikkat ti [[:$1]] manipud iti kategoria",
        "recentchanges-page-removed-from-category-bundled": "Naikkat ti [[:$1]] ken {{PLURAL:$2|maysa a panid|$2 a pampanid}} manipud iti kategoria",
+       "autochange-username": "Automatiko a panagbaliw iti MediaWiki",
        "upload": "Agikarga iti papeles",
        "uploadbtn": "Agikarga iti papeles",
        "reuploaddesc": "Ukasen ti panagikarga ken agsubli idiay porma ti panagikarga",
        "upload-form-label-infoform-description": "Deskripsion",
        "upload-form-label-usage-title": "Panagusar",
        "upload-form-label-usage-filename": "Nagan ti papeles",
+       "foreign-structured-upload-form-label-own-work": "Daytoy ket bukodko nga obra",
+       "foreign-structured-upload-form-label-infoform-categories": "Katkategoria",
+       "foreign-structured-upload-form-label-infoform-date": "Petsa",
        "backend-fail-stream": "Saan a maipan ti papeles $1.",
        "backend-fail-backup": "Saan a makaidulin ti kapada ti papeles ti $1.",
        "backend-fail-notexists": "Awan ti papeles ti $1.",
        "wlnote": "Dita baba ket {{PLURAL:$1|naudi a sinukatan|dagiti naudi a <strong>$1</strong> a sinukatan}} iti napalabas {{PLURAL:$2|nga oras|a <strong>$2</strong> nga or-oras}}, manipud idi $3, $4.",
        "wlshowlast": "Ipakita dagiti naudi a $1 nga or-oras $2 nga al-aldaw",
        "watchlistall2": "amin",
+       "watchlist-hide": "Ilemmeng",
+       "wlshowtime": "Ipakita ti naudi:",
+       "wlshowhideminor": "dagiti bassit a panagurnos",
+       "wlshowhidebots": "dagiti bot",
+       "wlshowhideliu": "dagiti nakarehistro nga agar-aramat",
+       "wlshowhideanons": "dagiti di ammo nga agar-aramat",
+       "wlshowhidepatr": "dagiti napatrulian a panagurnos",
+       "wlshowhidemine": "dagiti inurnosko",
        "watchlist-options": "Dagiti pagpilian ti listaan a bambantayan",
        "watching": "Bambantayan...",
        "unwatching": "Saanen a bantayan...",
        "contributions": "Dagiti kontribusion ti {{GENDER:$1|agar-aramat}}",
        "contributions-title": "Kontribusion ti agar-aramat para kenni $1",
        "mycontris": "Inar-aramid",
+       "anoncontribs": "Inar-aramid",
        "contribsub2": "Para kenni {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Ti pakabilangan ti agar-aramat ni \"$1\" ket saan a nakarehistro.",
        "nocontribs": "Awan ti nasarakan a nasukatan a kapada dagitoy a kriteria.",
        "cant-move-to-user-page": "Awan ti pammalubosmo nga agiyalis ti panid iti panid ti agar-aramat (malaksid kadagiti subpanid ti agar-aramat).",
        "cant-move-category-page": "Awan ti pammalubosmo nga agiyalis kadagiti panid ti kategoria.",
        "cant-move-to-category-page": "Awan ti pammalubosmo nga agiyalis ti panid iti panid ti kategoria.",
-       "newtitle": "Iti baro a titulo:",
+       "newtitle": "Baro a titulo:",
        "move-watch": "Bantayan ti taudan a panid ken puntaan a panid",
        "movepagebtn": "Iyalis ti panid",
        "pagemovedsub": "Balligi ti panangiyalis",
        "movenosubpage": "Daytoy a panid ket awan ti subpanidna.",
        "movereason": "Rason:",
        "revertmove": "isubli",
-       "delete_and_move": "Ikkaten ken iyalis",
        "delete_and_move_text": "== Masapul nga ikkaten ==\nTi pangipanan ti panid ket \"[[:$1]]\" addan.\nKayatmo nga ikkaten tapno makaiyaliska?",
        "delete_and_move_confirm": "Wen, ikkaten ti panid",
        "delete_and_move_reason": "Naikkat tapno mawayaan ti pannaka-iyalis manipud ti \"[[$1]]\"",
        "tooltip-pt-userpage": "Panidmo nga agar-aramat",
        "tooltip-pt-anonuserpage": "Ti panid ti agar-aramat para iti daytoy nga IP a pagtaengan a kas ur-urnosem",
        "tooltip-pt-mytalk": "Tungtungam a panid",
-       "tooltip-pt-anontalk": "Pakitungtungan a maipanggep kadagiti panagurnos manipud ti daytoy nga IP a pagtaengan",
+       "tooltip-pt-anontalk": "Pakitungtungan a maipanggep kadagiti panagurnos manipud iti daytoy nga adres ti IP",
        "tooltip-pt-preferences": "Dagiti kakaykayatam",
        "tooltip-pt-watchlist": "Listaan dagiti panid a sipsiputem para iti pannakabalbaliw",
        "tooltip-pt-mycontris": "Ti listaan dagiti kontribusionmo",
+       "tooltip-pt-anoncontribs": "Ti listaan dagiti panagurnos manipud iti daytoy nga adres ti IP",
        "tooltip-pt-login": "Maaw-awis a sumrekka; nupay kasta, daytoy ket saan a nasken",
        "tooltip-pt-logout": "Rummuar",
        "tooltip-pt-createaccount": "Maaw-awis nga agpartuatka iti pakabilangan ken sumrek; nupay kasta, daytoy ket saan a nasken",
index bafbadb..f353082 100644 (file)
        "tog-prefershttps": "Usa sempre una connessione sicura quando si effettua l'accesso",
        "underline-always": "Sempre",
        "underline-never": "Mai",
-       "underline-default": "Impostazioni predefinite del browser o della skin",
+       "underline-default": "Impostazioni predefinite del browser o del tema",
        "editfont-style": "Stile del carattere nella casella di modifica:",
        "editfont-default": "Predefinito del browser",
        "editfont-monospace": "Carattere a larghezza fissa",
        "morenotlisted": "Questo elenco non è completo.",
        "mypage": "Pagina",
        "mytalk": "discussioni",
-       "anontalk": "Discussioni per questo IP",
+       "anontalk": "discussioni",
        "navigation": "Navigazione",
        "and": "&#32;e",
        "qbfind": "Trova",
        "userjspreview": "'''Questa è solo un'anteprima per provare il proprio JavaScript personale; le modifiche non sono ancora state salvate!'''",
        "sitecsspreview": "Questa è solo un'anteprima del CSS. Le modifiche non sono ancora state salvate!'''",
        "sitejspreview": "Questa è solo un'anteprima per provare il JavaScript; le modifiche non sono ancora state salvate!'''",
-       "userinvalidcssjstitle": "'''Attenzione:'''  Non esiste alcuna skin con nome \"$1\". Si noti che le pagine per i .css e .js personalizzati hanno l'iniziale del titolo minuscola, ad esempio {{ns:user}}:Esempio/vector.css e non {{ns:user}}:Esempio/Vector.css.",
+       "userinvalidcssjstitle": "<strong>Attenzione:</strong> non esiste alcun tema con nome \"$1\". Si noti che le pagine per i .css e .js personalizzati hanno l'iniziale del titolo minuscola, ad esempio {{ns:user}}:Esempio/vector.css e non {{ns:user}}:Esempio/Vector.css.",
        "updated": "(Aggiornato)",
        "note": "'''Nota:'''",
        "previewnote": "'''Ricorda che questa è solo un'anteprima.'''\nLe tue modifiche NON sono ancora state salvate!",
        "mypreferences": "preferenze",
        "prefs-edits": "Modifiche effettuate:",
        "prefsnologintext2": "Per modificare le tue preferenze è necessario effettuare l'accesso.",
-       "prefs-skin": "Aspetto grafico (skin)",
+       "prefs-skin": "Tema",
        "skin-preview": "Anteprima",
        "datedefault": "Nessuna preferenza",
        "prefs-labs": "Funzionalità sperimentali",
        "prefs-files": "File",
        "prefs-custom-css": "CSS personalizzato",
        "prefs-custom-js": "JavaScript personalizzato",
-       "prefs-common-css-js": "CSS/JavaScript condiviso per tutte le skin:",
+       "prefs-common-css-js": "CSS/JavaScript condiviso per tutti i temi:",
        "prefs-reset-intro": "È possibile usare questa pagina per reimpostare le proprie preferenze a quelle predefinite del sito.\nL'operazione non può essere annullata.",
        "prefs-emailconfirm-label": "Conferma dell'e-mail:",
        "youremail": "Indirizzo email:",
        "contributions": "Contributi {{GENDER:$1|utente}}",
        "contributions-title": "Contributi di $1",
        "mycontris": "contributi",
+       "anoncontribs": "contributi",
        "contribsub2": "Per {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "L'utenza \"$1\" non è registrata.",
        "nocontribs": "Non sono state trovate modifiche che soddisfino i criteri di ricerca.",
        "javascripttest-pagetext-unknownframework": "Framework di test sconosciuto \"$1\".",
        "javascripttest-pagetext-unknownaction": "Azione sconosciuta \"$1\".",
        "javascripttest-pagetext-frameworks": "Per cortesia, scegli uno dei seguenti framework per i test: $1",
-       "javascripttest-pagetext-skins": "Scegli una skin con cui eseguire i test:",
+       "javascripttest-pagetext-skins": "Scegli un tema con cui eseguire i test:",
        "javascripttest-qunit-intro": "Vedi su mediawiki.org la [$1 documentazione riguardante i test].",
        "tooltip-pt-userpage": "La tua pagina utente",
        "tooltip-pt-anonuserpage": "La pagina utente di questo indirizzo IP",
        "tooltip-pt-preferences": "Le tue preferenze",
        "tooltip-pt-watchlist": "La lista delle pagine che stai tenendo sotto osservazione",
        "tooltip-pt-mycontris": "La lista dei tuoi contributi",
+       "tooltip-pt-anoncontribs": "Un elenco delle modifiche fatte da questo indirizzo IP",
        "tooltip-pt-login": "Si consiglia di effettuare l'accesso, anche se non è obbligatorio",
        "tooltip-pt-logout": "Uscita (logout)",
        "tooltip-pt-createaccount": "Si consiglia di registrarsi e di effettuare l'accesso, anche se non è obbligatorio",
        "tooltip-preferences-save": "Salva le preferenze",
        "tooltip-summary": "Inserire una breve sintesi",
        "interlanguage-link-title": "$1 - $2",
-       "common.css": "/* Gli stili CSS inseriti qui si applicano a tutte le skin */",
+       "common.css": "/* Gli stili CSS inseriti qui si applicano a tutti i temi */",
        "print.css": "/* Gli stili CSS inseriti qui si applicano all'output in stampa */",
        "noscript.css": "/ * Gli stili CSS inseriti qui si applicano agli utenti che hanno JavaScript disabilitato * /",
        "group-autoconfirmed.css": "/ * Gli stili CSS inseriti qui si applicheranno solo ad utenti autoconvalidati * /",
        "exif-datetimeexpires": "Non utilizzare dopo",
        "exif-datetimereleased": "Rilasciato il",
        "exif-originaltransmissionref": "Codice del luogo di trasmissione originaria",
-       "exif-identifier": "Identificativo",
+       "exif-identifier": "Identificatore",
        "exif-lens": "Obiettivo utilizzato",
        "exif-serialnumber": "Numero di serie della fotocamera",
        "exif-cameraownername": "Proprietario della macchina fotografica",
        "invalid-indicator-name": "<strong>Errore:</strong> attributo <code>name</code> degli indicatori dello stato della pagina non può essere vuoto.",
        "version": "Versione",
        "version-extensions": "Estensioni installate",
-       "version-skins": "Skin installate",
+       "version-skins": "Temi installati",
        "version-specialpages": "Pagine speciali",
        "version-parserhooks": "Hook del parser",
        "version-variables": "Variabili",
        "version-license": "Licenza MediaWiki",
        "version-ext-license": "Licenza",
        "version-ext-colheader-name": "Estensione",
-       "version-skin-colheader-name": "Skin",
+       "version-skin-colheader-name": "Tema",
        "version-ext-colheader-version": "Versione",
        "version-ext-colheader-license": "Licenza",
        "version-ext-colheader-description": "Descrizione",
        "log-name-pagelang": "Modifiche lingua",
        "log-description-pagelang": "Questo è un registro delle modifiche alla lingua delle pagine.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|ha modificato}} la lingua della pagina $3 da $4 a $5.",
-       "default-skin-not-found": "Oops! La skin predefinita per il tuo wiki, definita in <code dir=\"ltr\">$wgDefaultSkin</code> come <code>$1</code>, non è disponibile.\n\nLa tua installazione sembra includere {{PLURAL:$4|la seguente|le seguenti}} skin. Vedi [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manuale: configurazione skin] per informazioni su come abilitar{{PLURAL:$4|la|le e scegliere quella predefinita}}.\n\n$2\n\n; Se hai appena installato MediaWiki:\n: Probabilmente lo hai installato da git, o direttamente dal codice sorgente usando qualche altro metodo. Ciò era previsto. Prova ad installare alcune skin dalla [https://www.mediawiki.org/wiki/Category:All_skins directory su mediawiki.org], tramite:\n:* Scaricando il [https://www.mediawiki.org/wiki/Download programma di installazione tarball], che viene fornito con diverse skin ed estensioni. Puoi fare copia ed incolla della directory <code dir=\"ltr\">skins/</code> da lì.\n:* Scaricando tarball di singole skin da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usando Git per scaricare le skin].\n: In questo modo non dovrebbe interferire con il tuo repository git se sei uno sviluppatore MediaWiki.\n\n; Se hai appena aggiornato MediaWiki:\n: MediaWiki 1.24 e versioni successive non abilitano più automaticamente le skin installate (vedi [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manuale: rilevamento automatico skin]). Puoi copiare {{PLURAL:$5|la seguente linea|le seguenti linee}} nel <code>LocalSettings.php</code> per abilitare {{PLURAL:$5|la|tutte le}} skin {{PLURAL:$5|installata|installate}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Se hai appena modificato <code>LocalSettings.php</code>:\n: Ricontrolla i nomi delle skin per errori di battitura.",
-       "default-skin-not-found-no-skins": "Oops! La skin predefinita per il tuo wiki, definita in <code>$wgDefaultSkin</code> come <code>$1</code>, non è disponibile.\n\nNon hai skin installate.\n\n; Se hai appena installato o aggiornato MediaWiki:\n: Probabilmente lo hai installato da git, o direttamente dal codice sorgente usando qualche altro metodo. Ciò era previsto. MediaWiki 1.24 e versioni successive non include alcuna skin nel repository principale. Prova ad installare alcune skin dalla [https://www.mediawiki.org/wiki/Category:All_skins directory su mediawiki.org], tramite:\n:* Scaricando il [https://www.mediawiki.org/wiki/Download programma di installazione tarball], che viene fornito con diverse skin ed estensioni. Puoi fare copia ed incolla della directory <code>skins/</code> da lì.\n:* Scaricando tarball di singole skin da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usando Git per scaricare le skin].\n: In questo modo non dovrebbe interferire con il tuo repository git se sei uno sviluppatore MediaWiki. Vedi [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manuale: configurazione skin] per informazioni su come abilitarle e scegliere quella predefinita.",
+       "default-skin-not-found": "Oops! Il tema predefinito per il tuo wiki, definito in <code dir=\"ltr\">$wgDefaultSkin</code> come <code>$1</code>, non è disponibile.\n\nLa tua installazione sembra includere {{PLURAL:$4|il seguente|i seguenti}} temi. Vedi [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manuale: configurazione tema] per informazioni su come {{PLURAL:$4|abilitarlo|abilitarli e scegliere quello predefinito}}.\n\n$2\n\n; Se hai appena installato MediaWiki:\n: Probabilmente lo hai installato da git, o direttamente dal codice sorgente usando qualche altro metodo. Ciò era previsto. Prova ad installare alcuni temi dalla [https://www.mediawiki.org/wiki/Category:All_skins directory su mediawiki.org], tramite:\n:* Scaricando il [https://www.mediawiki.org/wiki/Download programma di installazione tarball], che viene fornito con diversi temi ed estensioni. Puoi fare copia ed incolla della directory <code dir=\"ltr\">skins/</code> da lì.\n:* Scaricando tarball di singoli temi da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usando Git per scaricare i temi].\n: In questo modo non dovrebbe interferire con il tuo repository git se sei uno sviluppatore MediaWiki.\n\n; Se hai appena aggiornato MediaWiki:\n: MediaWiki 1.24 e versioni successive non abilitano più automaticamente i temi installati (vedi [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manuale: rilevamento automatico temi]). Puoi copiare {{PLURAL:$5|la seguente linea|le seguenti linee}} nel <code>LocalSettings.php</code> per abilitare {{PLURAL:$5|il tema installato|tutti i temi installati}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Se hai appena modificato <code>LocalSettings.php</code>:\n: Ricontrolla i nomi dei temi per errori di battitura.",
+       "default-skin-not-found-no-skins": "Oops! Il tema predefinito per il tuo wiki, definito in <code>$wgDefaultSkin</code> come <code>$1</code>, non è disponibile.\n\nNon hai temi installati.\n\n; Se hai appena installato o aggiornato MediaWiki:\n: Probabilmente lo hai installato da git, o direttamente dal codice sorgente usando qualche altro metodo. Ciò era previsto. MediaWiki 1.24 e versioni successive non include alcun tema nel repository principale. Prova ad installare alcuni temi dalla [https://www.mediawiki.org/wiki/Category:All_skins directory su mediawiki.org], tramite:\n:* Scaricando il [https://www.mediawiki.org/wiki/Download programma di installazione tarball], che viene fornito con diversi temi ed estensioni. Puoi fare copia ed incolla della directory <code>skins/</code> da lì.\n:* Scaricando tarball di singoli temi da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Usando Git per scaricare i temi].\n: In questo modo non dovrebbe interferire con il tuo repository git se sei uno sviluppatore MediaWiki. Vedi [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manuale: configurazione temi] per informazioni su come abilitarle e scegliere quello predefinito.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (abilitata)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 ('''disabilitata''')",
        "mediastatistics": "Statistiche relative ai file multimediali",
index 5b556c5..b969407 100644 (file)
        "morenotlisted": "この一覧は完全ではありません。",
        "mypage": "ページ",
        "mytalk": "トーク",
-       "anontalk": "このIPアドレスについての議論",
+       "anontalk": "議論",
        "navigation": "案内",
        "and": "&#32;と",
        "qbfind": "検索",
index 3e2c6e7..a3291f4 100644 (file)
        "listusers-noresult": "Қатысушы табылған жоқ.",
        "listusers-blocked": "(бұғатталған)",
        "activeusers": "Белсенді қатысушылар тізімі",
-       "activeusers-intro": "Ð\91ұл Ñ\82Ñ\96зÑ\96м Ñ\81оңÒ\93Ñ\8b $1 {{PLURAL:$1|күнде|күнде}} Ò\9bандай Ð´Ð° Ð±Ñ\96Ñ\80 Ñ\96Ñ\81\99Ñ\80екеÑ\82 Ð¶Ð°Ñ\81аÒ\93ан Ò\9bаÑ\82Ñ\8bÑ\81Ñ\83Ñ\88Ñ\8bлаÑ\80 Ñ\82Ñ\96зÑ\96мÑ\96.",
+       "activeusers-intro": "Бұл соңғы $1 {{PLURAL:$1|күнде|күнде}} қандай да бір іс-әрекет жасаған қатысушылар тізімі.",
        "activeusers-count": "соңғы {{PLURAL:$3|күнде|$3 күнде}} $1 {{PLURAL:$1|әрекет|әрекет}}",
        "activeusers-from": "Мынадан басталатын қатысушыларды көрсет:",
        "activeusers-hidebots": "Боттарды жасыру",
index 417df25..45b9e74 100644 (file)
@@ -54,7 +54,8 @@
                        "Hwangjy9",
                        "Kurousagi",
                        "Macofe",
-                       "Yearning"
+                       "Yearning",
+                       "고솜"
                ]
        },
        "tog-underline": "링크에 밑줄:",
        "morenotlisted": "이 목록은 완성되지 않았습니다.",
        "mypage": "문서",
        "mytalk": "토론",
-       "anontalk": "이 IP 주소의 사용자와 토론",
+       "anontalk": "토론",
        "navigation": "둘러보기",
        "and": ",",
        "qbfind": "찾기",
        "contributions": "{{GENDER:$1|사용자}} 기여",
        "contributions-title": "$1 사용자의 기여",
        "mycontris": "기여",
+       "anoncontribs": "기여",
        "contribsub2": "{{GENDER:$3|$1}}($2)의 기여",
        "contributions-userdoesnotexist": "\"$1\" 사용자 계정은 등록되어 있지 않습니다.",
        "nocontribs": "지정한 조건과 일치하는 바뀜을 찾을 수 없습니다.",
        "tooltip-pt-preferences": "사용자 환경 설정",
        "tooltip-pt-watchlist": "주시문서에 대한 바뀜 목록",
        "tooltip-pt-mycontris": "내 기여의 목록",
+       "tooltip-pt-anoncontribs": "이 IP 주소의 편집 목록",
        "tooltip-pt-login": "꼭 로그인해야 하는 것은 아니지만, 로그인을 권장합니다.",
        "tooltip-pt-logout": "로그아웃",
        "tooltip-pt-createaccount": "계정을 만들고 로그인하는 것이 좋습니다; 하지만, 필수는 아닙니다",
index 663dbaa..abcf0d0 100644 (file)
@@ -24,7 +24,8 @@
                        "아라",
                        "Lesgles",
                        "StevenJ81",
-                       "Macofe"
+                       "Macofe",
+                       "Xð"
                ]
        },
        "tog-underline": "Versores linea denotandi:",
        "viewhelppage": "Videre auxilium",
        "categorypage": "Videre categoriam",
        "viewtalkpage": "Videre disputationem",
-       "otherlanguages": "Linguis aliis",
+       "otherlanguages": "In aliis linguis",
        "redirectedfrom": "(Redirectum de $1)",
        "redirectpagesub": "Pagina redirectionis",
        "lastmodifiedat": "Ultima mutatio: $2, $1.",
index d93bc2a..3b7e622 100644 (file)
        "morenotlisted": "Dës Lëscht ass net komplett.",
        "mypage": "Säit",
        "mytalk": "Diskussioun",
-       "anontalk": "Diskussioun fir dës IP Adress",
+       "anontalk": "Diskussioun",
        "navigation": "Navigatioun",
        "and": "&#32;a(n)",
        "qbfind": "Fannen",
        "contributions": "{{GENDER:$1|Benotzer}}kontributiounen",
        "contributions-title": "Kontributioune vum $1",
        "mycontris": "Kontributiounen",
+       "anoncontribs": "Kontributiounen",
        "contribsub2": "Fir {{GENDER:$3|den $1|d'$1|de Benotzer $1}} ($2)",
        "contributions-userdoesnotexist": "De Benotzerkont \"$1\" ass net registréiert.",
        "nocontribs": "Et goufe keng Ännerunge fonnt, déi dëse Kritèren entspriechen.",
index 5b53215..c38406a 100644 (file)
        "morenotlisted": "Овој список не е целосен.",
        "mypage": "Страница",
        "mytalk": "разговор",
-       "anontalk": "Разговор за оваа IP-адреса",
+       "anontalk": "Разговор",
        "navigation": "Навигација",
        "and": "&#32;и",
        "qbfind": "Најди",
        "contributions": "{{GENDER:$1|Придонеси на корисникот}}",
        "contributions-title": "Придонеси на корисникот $1",
        "mycontris": "придонеси",
+       "anoncontribs": "Придонеси",
        "contribsub2": "За {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Корисничката сметка „$1“ не е регистрирана.",
        "nocontribs": "Не се пронајдени промени што одговараат на овој критериум.",
        "tooltip-pt-preferences": "Ваши нагодувања",
        "tooltip-pt-watchlist": "Список на страници кои сте избрале да ги набљудувате.",
        "tooltip-pt-mycontris": "Список на ваши придонеси",
+       "tooltip-pt-anoncontribs": "Список на уредувања направени од оваа IP-адреса",
        "tooltip-pt-login": "Ви препорачуваме да се најавите, иако тоа не е задолжително.",
        "tooltip-pt-logout": "Одјавување",
        "tooltip-pt-createaccount": "Ви препорачуваме да направите сметка и да се најавите, иако тоа не е задолжително",
index 08dedd0..c2fdf32 100644 (file)
        "morenotlisted": "ही यादी पूर्ण नाही.",
        "mypage": "पान",
        "mytalk": "चर्चा",
-       "anontalk": "या à¤\85à¤\82à¤\95पतà¥\8dतà¥\8dयाà¤\9aà¥\87 à¤\9aरà¥\8dà¤\9aा à¤ªà¤¾à¤¨ à¤\89à¤\98डा",
+       "anontalk": "à¤\9aरà¥\8dà¤\9aा à¤ªà¤¾à¤¨",
        "navigation": "सुचालन",
        "and": "&#32;आणि",
        "qbfind": "शोधा",
        "contributions": "{{GENDER:$1|सदस्य}} योगदान",
        "contributions-title": "$1 साठी सदस्य-योगदान",
        "mycontris": "योगदान",
+       "anoncontribs": "योगदान",
        "contribsub2": "{{GENDER:$3|$1}} ($2) साठी",
        "contributions-userdoesnotexist": "सदस्यखाते \"$1\"  हे नोंदलेले नाही.",
        "nocontribs": "या मानदंडाशी जुळणारे बदल सापडले नाहीत.",
        "tooltip-pt-preferences": "तुमचा पसंतीक्रम",
        "tooltip-pt-watchlist": "तुम्ही पहारा दिलेल्या पानांची यादी",
        "tooltip-pt-mycontris": "तुमच्या योगदानांची यादी",
+       "tooltip-pt-anoncontribs": "या अंकपत्त्यावरुन झालेले संपादन",
        "tooltip-pt-login": "आपणांस सनोंद प्रवेशासाठी प्रोत्साहीत करण्यात येत आहे;अर्थातच, ते अनिवार्य नाही.",
        "tooltip-pt-logout": "सनोंद निर्गम",
        "tooltip-pt-createaccount": "आम्ही आपणास खाते उघडून सनोंद-प्रवेशास प्रोत्साहीत करत आहे;अर्थातच, ते अनिवार्य नाही.",
index 2373ab7..9b7557e 100644 (file)
        "createaccountreason": "Lí-iû:",
        "createacct-reason": "理由:",
        "createacct-reason-ph": "為啥物你欲開一另外一个口座?",
-       "createacct-captcha": "An-choân kiám-cha",
-       "createacct-imgcaptcha-ph": "Kā ē-kha lí khoàⁿ tio̍h--ê bûn-jī phah ji̍p lâi",
        "createacct-submit": "Khui lí--ê kháu-chō",
        "createacct-another-submit": "開另外一个口座",
        "createacct-benefit-heading": "{{SITENAME}} sī uī tio̍h chhin-chhiūⁿ lí--ê lâng chiah lâi kiàn-li̍p--ê.",
        "search-section": "(toān-lo̍h $1)",
        "searchall": "choân-pō·",
        "showingresults": "Ē-kha tùi #<b>$2</b> khai-sí hián-sī <b>$1</b> hāng kiat-kó.",
+       "search-nonefound": "揣無",
        "powersearch-legend": "Kiám-sek",
        "preferences": "Siat-tēng",
        "mypreferences": "Góa ê siat-tēng",
        "rightslogtext": "Chit-ê log lia̍t-chhut kái-piàn iōng-chiá koân-lī ê tōng-chok.",
        "action-edit": "Siu-kái chit ia̍h",
        "nchanges": "$1 {{PLURAL:$1|kái|kái}}",
+       "enhancedrc-history": "歷史",
        "recentchanges": "Chòe-kīn ê kái-piàn",
+       "recentchanges-legend": "最近改的選定",
        "recentchanges-summary": "Tī chiah wiki ia̍h tui-cha chòe-kīn ê kái-piàn.",
        "recentchanges-label-newpage": "Chit ê siu-kái ē sán-seng sin ia̍h",
        "recentchanges-label-minor": "Che sī sió siu-kái",
        "recentchanges-label-bot": "Che sī ki-khì-lâng kái--ê",
+       "recentchanges-label-unpatrolled": "這个編輯抑無巡視過",
+       "recentchanges-label-plusminus": "彼頁改了;精差的位元組",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (請參考[[Special:NewPages|新頁清單]])",
        "rcnotefrom": "Ē-kha sī <b>$2</b> kàu taⁿ ê kái-piàn (ke̍k-ke hián-sī <b>$1</b> hāng).",
        "rclistfrom": "Hián-sī tùi $3 $2 kàu taⁿ ê sin kái-piàn",
        "rcshowhideminor": "$1 sió siu-kái",
+       "rcshowhideminor-hide": "藏起來",
        "rcshowhidebots": "$1 機器儂",
+       "rcshowhidebots-show": "開",
        "rcshowhideliu": "$1 teng-ji̍p ê iōng-chiá",
+       "rcshowhideliu-hide": "藏起來",
        "rcshowhideanons": "$1 bû-bêng-sī",
+       "rcshowhideanons-hide": "藏起來",
        "rcshowhidemine": "$1 góa ê pian-chi̍p",
+       "rcshowhidemine-hide": "藏起來",
        "rclinks": "Hían-sī $2 ji̍t lāi siōng sin ê $1 hāng kái-piàn<br />$3",
        "diff": "Cheng-chha",
        "hist": "ls",
        "recentchangeslinked": "Siong-koan ê kái-piàn",
        "recentchangeslinked-feed": "Siong-koan ê kái-piàn",
        "recentchangeslinked-toolbox": "Siong-koan ê kái-piàn",
+       "recentchangeslinked-title": "佮 \"$1\" 相關的改變",
        "recentchangeslinked-summary": "這是有相接到指定頁(抑是指定分類的所有成員),而且最近家己頁的內容有改過的清單。\n遐的頁[[Special:Watchlist|佇你的監視清單]]會用<strong>粗體</strong> 標示。",
        "recentchangeslinked-page": "Ia̍h ê miâ:",
        "upload": "Kā tóng-àn chiūⁿ-bāng",
        "linkstoimage": "ē-kha {{PLURAL:$1|ê ia̍h}} ū iōng tio̍h chit ê iáⁿ-siōng:",
        "nolinkstoimage": "Bô poàⁿ ia̍h liân kàu chit tiuⁿ iáⁿ-siōng.",
        "sharedupload-desc-here": "Chit--ê $1--ê tóng-àn ū khó-lêng hō͘ kî-thaⁿ--ê choan-àn ēng tio̍h.\nChia sī chit--ê tóng-àn i--ê [$2 soat-bêng].",
+       "upload-disallowed-here": "你袂使換掉个檔案",
        "mimesearch": "MIME chhiau-chhoē",
        "unwatchedpages": "Bô lâng kàm-sī ê ia̍h",
        "listredirects": "Lia̍t-chhut choán-ia̍h",
        "watchlist-details": "Kàm-sī-toaⁿ ū {{PLURAL:$1|$1 ia̍h|$1 ia̍h}}, thó-lūn-ia̍h bô sǹg chāi-lāi.",
        "wlnote": "$3 $4: Ē-kha sī <strong>$2</strong> tiám-cheng í-lāi siōng sin ê <strong>$1</strong> ê kái-piàn.",
        "wlshowlast": "Hián-sī chêng $1 tiám-cheng $2 ji̍t",
+       "watchlistall2": "choân-pō͘",
        "deletepage": "Thâi ia̍h",
        "confirm": "Khak-tēng",
        "excontent": "lōe-iông sī: '$1'",
        "move-page-legend": "Sóa ia̍h",
        "movepagetext": "Ē-kha chit ê form> iōng lâi kái 1 ê ia̍h ê piau-tê (miâ-chheng); só·-ū siong-koan ê le̍k-sú ē tòe leh sóa khì sin piau-tê.\nKū piau-tê ē chiâⁿ-chò 1 ia̍h choán khì sin piau-tê ê choán-ia̍h.\nLiân khì kū piau-tê ê liân-kiat (link) bē khì tāng--tio̍h; ē-kì-tit chhiau-chhōe siang-thâu (double) ê a̍h-sī kò·-chiòng ê choán-ia̍h.\nLí ū chek-jīm khak-tēng liân-kiat kè-sio̍k liân tio̍h ūi.\n\nSin piau-tê nā í-keng tī leh (bô phian-chi̍p koè ê khang ia̍h, choán-ia̍h bô chún-sǹg), tō bô-hoat-tō· soá khì hia.\nChe piaú-sī nā ū têng-tâⁿ, ē-sái kā sin ia̍h soà tńg-khì goân-lâi ê kū ia̍h.\n\n'''SÈ-JĪ!'''\nTùi chē lâng tha̍k ê ia̍h lâi kóng, soá-ūi sī toā tiâu tāi-chì.\nLiâu--lo̍h-khì chìn-chêng, chhiáⁿ seng khak-tēng lí ū liáu-kái chiah-ê hiō-kó.",
        "movepagetalktext": "Siong-koan ê thó-lūn-ia̍h (chún ū) oân-nâ ē chū-tōng tòe leh sóa-ūi. Í-hā ê chêng-hêng '''bô chún-sǹg''': *Beh kā chit ia̍h tùi 1 ê miâ-khong-kan (namespace) soá khì lēng-gōa 1 ê miâ-khong-kan, *Sin piau-tê í-keng ū iōng--kòe ê thó-lūn-ia̍h, he̍k-chiá *Ē-kha ê sió-keh-á bô phah-kau. Í-siōng ê chêng-hêng nā-chún tī leh, lí chí-hó iōng jîn-kang ê hong-sek sóa ia̍h a̍h-sī kā ha̍p-pèng (nā ū su-iàu).",
-       "movearticle": "Sóa ia̍h:",
        "movenologintext": "Lí it-tēng ài sī chù-chheh ê iōng-chiá jī-chhiáⁿ ū [[Special:UserLogin|teng-ji̍p]] chiah ē-tàng sóa ia̍h.",
        "newtitle": "Khì sin piau-tê:",
        "move-watch": "Kàm-sī chit ia̍h",
        "tooltip-t-permalink": "Chi̍t ia̍h kái--koè pán-pún ê éng-kiú liân-kiat",
        "tooltip-ca-nstab-main": "khoàⁿ ia̍h ê loē-iông",
        "tooltip-ca-nstab-user": "Khoàⁿ iōng-chiá ê Ia̍h",
-       "tooltip-ca-nstab-special": "這是一个特殊頁,你袂使改這頁。",
+       "tooltip-ca-nstab-special": "這是一个特殊頁,袂使改得。",
        "tooltip-ca-nstab-image": "Khoàⁿ tóng-àn ia̍h",
        "tooltip-ca-nstab-category": "Khoàⁿ lūi-pia̍t ia̍h",
        "tooltip-minoredit": "記這是一个小改",
        "ilsubmit": "Kiám-sek",
        "bydate": "chiàu ji̍t-kî",
        "metadata": "元資料",
+       "metadata-help": "這个檔案有伊的資訊,可能是相機抑掃描機用的。\n若改過這个檔案,資訊就無完全對著。",
        "metadata-expand": "Hián-sī iù-chiat",
        "metadata-collapse": "Am iù-chiat",
        "metadata-fields": "佇顯示圖片的頁,若掀開元資料,下跤的EXIF資料會儂看著。其他的元資料是先看無。\n* 廠商\n* 機型\n* 翕像的時陣\n* 曝光\n* 光圈\n* ISO 速率\n* 焦距\n* 作者\n* 版權\n* 說明\n* 緯度(GPS)\n* 經度(GPS)\n* 海拔(GPS)",
index 7cc9511..eaed165 100644 (file)
        "morenotlisted": "Chisto elenco nun è cumpreto.",
        "mypage": "Paggena",
        "mytalk": "'E chiàcchieriate mmie",
-       "anontalk": "Chiacchierate pe chisto IP",
+       "anontalk": "Chiacchierate",
        "navigation": "Navigazzione",
        "and": "&#32;e",
        "qbfind": "Truòva",
        "media_tip": "Cullegamente a file multimediale",
        "sig_tip": "Firma cu data e ora",
        "hr_tip": "Linea orizzontale (ausà cu gedizzio)",
-       "summary": "Innece",
+       "summary": "Riepilego:",
        "subject": "Suggietto:",
        "minoredit": "Chisto è nu cagnamiénto piccerillo",
        "watchthis": "Tiene d'uocchio chesta paggena",
        "contributions": "Contribbute {{GENDER:$1|utente}}",
        "contributions-title": "Cuntribbute 'a l'utente pe' $1",
        "mycontris": "'E ffatiche d''e mmeje",
+       "anoncontribs": "Cuntribbute",
        "contribsub2": "Ppe {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "'O cunto utente \"$1\" nun è riggistrato.",
        "nocontribs": "Nisciunu cagnamiento è stato truvato cu sti criterie.",
        "tooltip-pt-preferences": "Preferenze d''e mmeje",
        "tooltip-pt-watchlist": "'A lista d' 'e paggene ca state a cuntrullà",
        "tooltip-pt-mycontris": "Elenco dde tuje contributte",
+       "tooltip-pt-anoncontribs": "N'elenco 'e cagnamiente fatte 'a st'indirizzo IP",
        "tooltip-pt-login": "A reggistrazione è cunsigliata",
        "tooltip-pt-logout": "Jésce (logout)",
        "tooltip-pt-createaccount": "Pigliateve curaggio e criate n'utente e trasìte; ancora ca chisto nun s'avesse 'a ffà pe' fforza",
index 1217aa5..7c985ee 100644 (file)
        "morenotlisted": "Deze lijst is niet compleet.",
        "mypage": "Gebruikerspagina",
        "mytalk": "Overleg",
-       "anontalk": "Overlegpagina voor dit IP-adres",
+       "anontalk": "Overleg",
        "navigation": "Navigatie",
        "and": "&#32;en",
        "qbfind": "Zoeken",
        "contributions": "{{GENDER:$1|Gebruikersbijdragen}}",
        "contributions-title": "Bijdragen van $1",
        "mycontris": "Bijdragen",
+       "anoncontribs": "Bijdragen",
        "contribsub2": "Voor {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "De account \"$1\" is niet geregistreerd.",
        "nocontribs": "Geen wijzigingen gevonden die aan de gestelde criteria voldoen.",
index 0825cc0..00bc20b 100644 (file)
        "wlheader-showupdated": "Sider som har vorte endra sidan du sist såg på dei er '''utheva'''",
        "wlnote": "Nedanfor er {{PLURAL:$1|den siste endringa|dei siste '''$1''' endringane}} {{PLURAL:$2|den siste timen|dei siste '''$2''' timane}}, for $3, kl. $4.",
        "wlshowlast": "Vis siste $1 timane $2 dagane",
-       "watchlistall2": "alle",
+       "watchlistall2": "alt",
+       "watchlist-hide": "Gøym",
+       "wlshowtime": "Vis siste:",
+       "wlshowhideminor": "småplukk",
+       "wlshowhidebots": "robotar",
+       "wlshowhideliu": "registrerte brukarar",
+       "wlshowhideanons": "anonyme brukarar",
+       "wlshowhidepatr": "patruljerte endringar",
+       "wlshowhidemine": "mine endringar",
        "watchlist-options": "Alternativ for overvakingslista",
        "watching": "Overvakar...",
        "unwatching": "Fjernar frå overvakinglista...",
        "movenosubpage": "Denne sida har ingen undersider.",
        "movereason": "Årsak:",
        "revertmove": "flytt attende",
-       "delete_and_move": "Slett og flytt",
        "delete_and_move_text": "== Sletting påkravd ==\n\nMålsida «[[:$1]]» finst allereie. Vil du slette ho for å gje rom for flytting?",
        "delete_and_move_confirm": "Ja, slett sida",
        "delete_and_move_reason": "Sletta for å gje rom for flytting frå «[[$1]]»",
index fa7e2fe..4af83d6 100644 (file)
@@ -14,7 +14,8 @@
                        "לערי ריינהארט",
                        "아라",
                        "Macofe",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "Xð"
                ]
        },
        "tog-underline": "Soslinhar los ligams :",
        "viewhelppage": "Vejatz la pagina d'ajuda",
        "categorypage": "Vejatz la pagina de las categorias",
        "viewtalkpage": "Pagina de discussion",
-       "otherlanguages": "Autras lengas",
+       "otherlanguages": "En autras lengas",
        "redirectedfrom": "(Redirigit dempuèi $1)",
        "redirectpagesub": "Pagina de redireccion",
        "redirectto": "Redirigir cap a :",
        "createaccountreason": "Motiu :",
        "createacct-reason": "Motiu",
        "createacct-reason-ph": "Perqué creatz un autre compte",
-       "createacct-captcha": "Contraròtle de seguretat",
-       "createacct-imgcaptcha-ph": "Entratz lo tèxte que vesètz çaisús",
        "createacct-submit": "Creatz vòstre compte",
        "createacct-another-submit": "Crear un autre compte",
        "createacct-benefit-heading": "{{SITENAME}} es escrich per de monde coma vos.",
        "wlheader-showupdated": "Las paginas que son estadas modificadas dempuèi vòstra darrièra visita son afichadas en '''gras'''.",
        "wlnote": "Çaijós {{PLURAL:$1|figura la darrièra modificacion efectuada|figuran las <strong>$1</strong> darrièras modificacions efectuadas}} pendent {{PLURAL:$2|la darrièra ora|las <strong>$2</strong> darrièras oras}}, dempuèi $3, $4.",
        "wlshowlast": "Far veire las darrièras $1 oras, los darrièrs $2 jorns",
+       "watchlistall2": "tot",
        "watchlist-options": "Opcions de la lista de seguiment",
        "watching": "Seguit...",
        "unwatching": "Fin del seguit...",
        "movenosubpage": "Aquesta pagina a pas cap de sospagina.",
        "movereason": "Motiu :",
        "revertmove": "anullar",
-       "delete_and_move": "Suprimir e tornar nomenar",
        "delete_and_move_text": "==Supression requerida==\nL’article de destinacion « [[:$1]] » existís ja.\nLo volètz suprimir per permetre lo cambiament de nom ?",
        "delete_and_move_confirm": "Òc, accèpti de suprimir la pagina de destinacion per permetre lo cambiament de nom.",
        "delete_and_move_reason": "Pagina suprimida per permetre lo cambiament de nom dempuèi « [[$1]] »",
index 067a904..56ac0e2 100644 (file)
        "morenotlisted": "Nie jest to kompletna lista.",
        "mypage": "Strona",
        "mytalk": "Dyskusja",
-       "anontalk": "Dyskusja tego IP",
+       "anontalk": "Dyskusja",
        "navigation": "Nawigacja",
        "and": "&#32;oraz",
        "qbfind": "Znajdź",
        "contributions": "Wkład {{GENDER:$1|użytkownika|użytkowniczki}}",
        "contributions-title": "Wkład {{GENDER:$1|użytkownika|użytkowniczki}} $1",
        "mycontris": "Edycje",
+       "anoncontribs": "Edycje",
        "contribsub2": "Dla {{GENDER:$3|użytkownika|użytkowniczki}} $1 ($2)",
        "contributions-userdoesnotexist": "Konto użytkownika „$1” nie jest zarejestrowane.",
        "nocontribs": "Brak zmian odpowiadających tym kryteriom.",
        "tooltip-pt-preferences": "Moje preferencje",
        "tooltip-pt-watchlist": "Lista stron obserwowanych przez Ciebie",
        "tooltip-pt-mycontris": "Lista moich edycji",
+       "tooltip-pt-anoncontribs": "Lista edycji wykonanych z tego adresu IP",
        "tooltip-pt-login": "Zachęcamy do zalogowania się, choć nie jest to obowiązkowe.",
        "tooltip-pt-logout": "Wyloguj",
        "tooltip-pt-createaccount": "Zachęcamy do stworzenia konta i zalogowania, ale nie jest to konieczne.",
index aaec4bb..9e53166 100644 (file)
        "morenotlisted": "دا لړليک بشپړ نه دی",
        "mypage": "زما مخ",
        "mytalk": "خبرې اترې",
-       "anontalk": "ددÛ\90 IP Ø®Ø¨Ø±Û\90 Ø§ØªØ±Û\90",
+       "anontalk": "خبرې اترې",
        "navigation": "گرځښت",
        "and": "&#32;او",
        "qbfind": "موندل",
        "contributions": "{{GENDER:$1|کارن}} ونډې",
        "contributions-title": "د $1 کارن ونډې",
        "mycontris": "ونډې",
+       "anoncontribs": "ونډې",
        "contribsub2": "د {{GENDER:$3|$1}} لپاره ($2)",
        "contributions-userdoesnotexist": "کارن گڼون \"$1\" نه دی ثبت شوی.",
        "nocontribs": "دې شرطونو سره سم بدلونونه و نه موندل شول.",
index 754c9ac..ddefe9e 100644 (file)
        "morenotlisted": "Esta lista não está completa.",
        "mypage": "Página",
        "mytalk": "Discussão",
-       "anontalk": "Discussão para este IP",
+       "anontalk": "Discussão",
        "navigation": "Navegação",
        "and": "&#32;e",
        "qbfind": "Procurar",
        "title-invalid-interwiki": "O título da página solicitada contém uma ligação interlíngua que não pode ser utilizada em títulos.",
        "title-invalid-talk-namespace": "O título da página solicitada refere-se a uma página de discussão que não existe.",
        "title-invalid-characters": "O título da página solicitada contém carateres inválidos: \"$1\".",
+       "title-invalid-magic-tilde": "O título da página solicitada possui uma sequência de tis inválida (<nowiki>~~~</nowiki>).",
        "title-invalid-too-long": "O título da página solicitada é demasiado longo. Não deverá ser maior que $1 {{PLURAL:$1|byte|bytes}} na codificação UTF-8.",
        "title-invalid-leading-colon": "O título da página solicitada contém um erro de pontuação (:) no início.",
        "perfcached": "Os seguintes dados encontram-se armazenados na ''cache'' e podem não estar atualizados. No máximo {{PLURAL:$1|um resultado é disponível|$1 resultados são disponíveis}} na ''cache''.",
        "permissionserrors": "Erro de permissão",
        "permissionserrorstext": "Não possui permissão para fazer isso, {{PLURAL:$1|pelo seguinte motivo|pelos seguintes motivos}}:",
        "permissionserrorstext-withaction": "Não possui permissão para $2, {{PLURAL:$1|pelo seguinte motivo|pelos seguintes motivos}}:",
+       "contentmodelediterror": "Não pode editar esta revisão pois seu modelo de conteúdo é <code>$1</code>, e o modelo actual do conteúdo da página é <code>$2</code>.",
        "recreate-moveddeleted-warn": "'''Aviso: Está a recriar uma página anteriormente eliminada.'''\n\nVerifique se é apropriado continuar a editar esta página.\nPara sua conveniência, é apresentado de seguida o registo de eliminação e de movimento da página:",
        "moveddeleted-notice": "Esta página foi eliminada.\nPara referência, é apresentado de seguida o registo de eliminações e de movimento da página.",
        "moveddeleted-notice-recent": "Desculpe, esta página foi eliminada recentemente (nas últimas 24 horas).\nA exclusão e registo de movimentação para a página são fornecidos abaixo para referência.",
        "showingresultsinrange": "Apresenta-se abaixo {{PLURAL:$1|<strong>1</strong> resultado|até <strong>$1</strong> resultados}} no intervalo #<strong>$2</strong> a #<strong>$3</strong>.",
        "search-showingresults": "{{PLURAL:$4|Resultado <strong>$1</strong> de <strong>$3</strong>|Resultados <strong>$1 - $2</strong> de <strong>$3</strong>}}",
        "search-nonefound": "A pesquisa não produziu resultados.",
+       "search-nonefound-thiswiki": "Não existem resultados que correspondam à consulta neste sítio.",
        "powersearch-legend": "Pesquisa avançada",
        "powersearch-ns": "Pesquisar nos domínios:",
        "powersearch-togglelabel": "Marcar:",
        "recentchanges-page-added-to-category-bundled": "[[:$1]] e {{PLURAL:$2|uma outra página|$2 outras páginas}} foram adicionadas à categoria",
        "recentchanges-page-removed-from-category": "[[:$1]] foi removida da categoria",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] e {{PLURAL:$2|uma outra página|$2 outras páginas}} foram removidas da categoria",
+       "autochange-username": "Alteração automática do MediaWiki",
        "upload": "Carregar ficheiro",
        "uploadbtn": "Carregar ficheiro",
        "reuploaddesc": "Cancelar o envio e voltar ao formulário de carregamento",
index dcf26b3..2319663 100644 (file)
        "tog-watchlisthidebots": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide bot edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthideminor": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide minor edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthideliu": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
+       "tog-watchlistreloadautomatically": "[[Special:Preferences]], tab 'Watchlist'. Offers user to to automatically refresh the watchlist page, when a filter is changed.",
        "tog-watchlisthideanons": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthidepatrolled": "Option in Watchlist tab of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthidecategorization": "Option in Watchlist tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages. Appears next to checkboxes with labels such as {{msg-mw|tog-watchlisthideminor}}.",
index f0d4da5..6635c0f 100644 (file)
        "morenotlisted": "Această listă nu este completă.",
        "mypage": "Pagină",
        "mytalk": "Discuții",
-       "anontalk": "Discuția pentru această adresă IP",
+       "anontalk": "Discuții",
        "navigation": "Navigare",
        "and": "&#32;și",
        "qbfind": "Găsește",
        "wlnote": "Mai jos se află {{PLURAL:$1|ultima schimbare|ultimele <strong>$1</strong> schimbări|ultimele <strong>$1</strong> de schimbări}} din {{PLURAL:$2|ultima oră|ultimele <strong>$2</strong> ore|ultimele <strong>$2</strong> de ore}}, așa cum era situația la $3, $4.",
        "wlshowlast": "Arată ultimele $1 ore $2 zile",
        "watchlistall2": "toate",
+       "watchlist-hide": "Ascunde",
        "wlshowtime": "Arată ultimele:",
        "wlshowhideminor": "modificări minore",
        "wlshowhidebots": "roboți",
        "contributions": "Contribuții {{GENDER:$1|utilizator}}",
        "contributions-title": "Contribuțiile utilizatorului $1",
        "mycontris": "Contribuții",
+       "anoncontribs": "Contribuții",
        "contribsub2": "Pentru {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Contul de utilizator „$1” nu este înregistrat.",
        "nocontribs": "Nu a fost găsită nici o modificare care să satisfacă acest criteriu.",
        "tooltip-pt-preferences": "Preferințele dumneavoastră",
        "tooltip-pt-watchlist": "Lista paginilor pe care le monitorizați",
        "tooltip-pt-mycontris": "Listă de contribuții",
+       "tooltip-pt-anoncontribs": "O listă de modificări efectuate de la această adresă IP",
        "tooltip-pt-login": "Sunteți încurajat să vă autentificați, deși acest lucru nu este obligatoriu.",
        "tooltip-pt-logout": "Închide sesiunea de lucru",
        "tooltip-pt-createaccount": "Vă încurajăm să vă creați un cont și să vă autentificați; totuși, nu este obligatoriu",
        "size-bytes": "{{PLURAL:$1|un octet|$1 octeți|$1 de octeți}}",
        "size-pixel": "$1 {{PLURAL:$1|pixel|pixeli|de pixeli}}",
        "lag-warn-normal": "Modificările mai noi de $1 {{PLURAL:$1|secondă|seconde}} pot să nu apară în listă.",
-       "lag-warn-high": "Serverul bazei de date este suprasolicitat, astfel încât modificările făcute în ultimele $1 {{PLURAL:$1|secundă|secunde}} pot să nu apară în listă.",
+       "lag-warn-high": "Serverul bazei de date este suprasolicitat, astfel încât modificările făcute în ultimele $1 {{PLURAL:$1|secundă|secunde|de secunde}} ar putea să nu apară în listă.",
        "watchlistedit-normal-title": "Modificare listă pagini urmărite",
        "watchlistedit-normal-legend": "Ștergere titluri din lista de urmărire",
        "watchlistedit-normal-explain": "Lista de mai jos cuprinde paginile pe care le urmăriți.\nPentru a elimina un titlu, bifați-l și apăsați „{{int:Watchlistedit-normal-submit}}”.\nPuteți modifica și direct [[Special:EditWatchlist/raw|lista brută]].",
        "logentry-suppress-block": "$1 {{GENDER:$2|a blocat}} utilizatorul {{GENDER:$4|$3}} pe o perioadă de $5 $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|a schimbat}} parametrii blocării pentru utilizatorul {{GENDER:$4|$3}} cu o perioadă de expirare de $5 $6",
        "logentry-import-upload": "$1 {{GENDER:$2|a importat}} $3 prin încărcare de fișier",
+       "logentry-import-upload-details": "$1 {{GENDER:$2|a importat}} $3 prin încărcare de fișier ($4 {{PLURAL:$4|versiune|versiuni|de versiuni}})",
        "logentry-import-interwiki": "$1 {{GENDER:$2|a importat}} $3 din alt wiki",
+       "logentry-import-interwiki-details": "$1 {{GENDER:$2|a importat}} $3 de la $5 ($4 {{PLURAL:$4|versiune|versiuni|de versiuni}})",
        "logentry-merge-merge": "$1 {{GENDER:$2|a unificat}} $3 cu $4 (versiuni de până la $5)",
        "logentry-move-move": "$1 {{GENDER:$2|a redenumit}} pagina $3 în $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|a redenumit}} pagina $3 în $4 fără a lăsa o redirecționare în loc",
index 21fd01b..ec61e6d 100644 (file)
        "morenotlisted": "Этот список неполон.",
        "mypage": "Страница",
        "mytalk": "Обсуждение",
-       "anontalk": "Обсуждение для этого IP-адреса",
+       "anontalk": "Обсуждение",
        "navigation": "Навигация",
        "and": "&#32;и",
        "qbfind": "Поиск",
        "contributions": "Вклад {{GENDER:$1|участника|участницы}}",
        "contributions-title": "Вклад {{GENDER:$1|участника|участницы}} $1",
        "mycontris": "Вклад",
+       "anoncontribs": "Вклад",
        "contribsub2": "Вклад {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Не зарегистрировано учётной записи «$1».",
        "nocontribs": "Изменений, соответствующих заданным условиям, найдено не было.",
        "tooltip-pt-preferences": "Ваши настройки",
        "tooltip-pt-watchlist": "Список страниц, изменения в которых вы отслеживаете",
        "tooltip-pt-mycontris": "Список ваших правок",
+       "tooltip-pt-anoncontribs": "Список правок, сделанных с этого IP-адреса",
        "tooltip-pt-login": "Здесь можно зарегистрироваться в системе, но это необязательно.",
        "tooltip-pt-logout": "Завершить сеанс работы",
        "tooltip-pt-createaccount": "Мы предлагаем вам создать учётную запись и войти в систему, хотя это и не обязательно.",
index 614a877..2a53b49 100644 (file)
        "morenotlisted": "Бу тиһик толорута суох.",
        "mypage": "Сирэй",
        "mytalk": "Кэпсэтэр сирим",
-       "anontalk": "Ð\91Ñ\83 IP-га Ñ\8bрытыы",
+       "anontalk": "Ырытыы",
        "navigation": "Навигация",
        "and": "&#32;уонна",
        "qbfind": "Бул",
        "movenosubpage": "Бу сирэй алын сирэйэ суох.",
        "movereason": "Төрүөтэ:",
        "revertmove": "төннөрүү",
-       "delete_and_move": "Суох гын уонна аатын уларыт",
        "delete_and_move_text": "==Сотуохха наада==\n\nМаннык ааттаах сирэй [[:$1|«$1»]] бэлиэр баар. Эн ону суох гынан баран аатын уларытаары гынаҕын дуо?",
        "delete_and_move_confirm": "Сөп, бу сирэйи суох гын",
        "delete_and_move_reason": "Аатын уларытаары сотулунна \"[[$1]]\"",
index c696433..17d95e8 100644 (file)
        "morenotlisted": "فهرست مڪمل ڪانهي.",
        "mypage": "منهنجو صفحو",
        "mytalk": "ڳالهہ ٻول",
-       "anontalk": "هن آءِ پِي پتي لاءِ مباحثي صفحو",
+       "anontalk": "ڳالھ ٻولھ",
        "navigation": "رهنمائي",
        "and": "&#32؛۽",
        "qbfind": "ڳوليو",
        "history-feed-description": "وڪي جي هن صفحي جي ترميمي سوانح",
        "history-feed-item-nocomment": "$2 تي $1",
        "rev-deleted-user": "(يُوزرنانءُ ڊاٺو ويو)",
+       "rev-delundel": "نمائش تبديل ڪريو",
        "rev-showdeleted": "ڏيکاريو",
        "revisiondelete": "مسوادا ڊاهيو/اڻ‌ڊاهيو",
        "revdelete-no-file": "ڄاڻايل فائيل وجود نہ ٿو رکي.",
        "revdelete-show-file-submit": "ها",
+       "revdelete-legend": "نمائش جون پابنديون ترتيب ڪريو",
        "revdelete-hide-image": "فائيل جو مواد لڪايو",
        "revdelete-hide-comment": "ترميم جو تتُ",
        "revdelete-hide-user": "ايڊيٽر جو يوزرنانءُ / آء پي پتو",
        "revdelete-radio-set": "لڪل",
        "revdelete-radio-unset": "ظاهر",
        "revdelete-log": "سبب:",
+       "revdel-restore": "نمائش تبديل ڪريو",
        "pagehist": "صفحي جي سوانح",
        "deletedhist": "ڊاٺل سوانح",
        "revdelete-otherreason": "ٻيا/اضافي ڪارڻ:",
        "revertmerge": "اڻ ضم",
        "history-title": "\"$1\" جي ترميمي سوانح",
        "difference-title": "\"$1\" جي مسودن ۾ تفاوت",
-       "difference-title-multipage": "صفحن \"$1\" ۽ \"$1\" ۾ تفاوت",
+       "difference-title-multipage": "صفحن \"$1\" ۽ \"$2\" ۾ تفاوت",
        "difference-multipage": "(صفحن درميان تفاوت)",
        "lineno": "سِٽَ $1:",
        "compareselectedversions": "چونڊيل پرت ڀيٽيو",
        "diff-empty": "(ڪو بہ تفاوت ڪونهي)",
        "searchresults": "ڳولا نتيجا",
        "searchresults-title": "”$1“ لاءِ ڳولا نتيجا",
+       "titlematches": "صفحي جو عنوان مشابهت رکي ٿو",
+       "textmatches": "صفحي جو متن مشابهت رکي ٿو",
+       "notextmatches": "ڪنهن به صفحي جو متن مشابهت نٿو رکي",
        "prevn": "پويان {{PLURAL:$1|$1}}",
        "nextn": "اڳيان {{PLURAL:$1|$1}}",
        "prev-page": "اڳوڻو صفحو",
        "searchrelated": "لاڳاپيل",
        "searchall": "سڀ",
        "search-nonefound": "توهان جي ڳولا جي نتيجي ۾ ڪجھہ بہ ڪو نہ لڌو.",
+       "powersearch-legend": "اعليٰ ڳولا",
        "powersearch-togglelabel": "چڪاسيو:",
        "powersearch-toggleall": "سڀ",
        "powersearch-togglenone": "ڪو بہ نہ",
        "prefs-watchlist-days-max": "وڌ ۾ وڌ $1 {{PLURAL:$1|ڏينهن}}",
        "prefs-watchlist-edits-max": "وڌ ۾ وڌ تعداد: 1000",
        "prefs-watchlist-token": "ٽيٽ لسٽ جو ٽوڪن:",
+       "prefs-misc": "متفرق",
        "prefs-resetpass": "ڳجھو لفظ بدلايو",
        "prefs-changeemail": "برق ٽپال پتو مِٽايو يا بدلايو",
        "prefs-setemail": "ڪو برق ٽپال پتو ڄاڻايو",
        "searchresultshead": "ڳولا",
        "stub-threshold-sample-link": "نمونو",
        "stub-threshold-disabled": "غيرفعال",
+       "recentchangesdays": "تازين تبديلين ۾ ڏيکارڻ جي لاءِ ڏينهن:",
        "recentchangesdays-max": "وڌ ۾ وڌ $1 {{PLURAL:$1|ڏينهن}}",
+       "recentchangescount": "عدم پيروي جي صورت ۾ ڏيکارڻ جي لاءِ ترميمون:",
        "prefs-help-recentchangescount": "ان ۾ تازيون تبديليون، صفحن جي سوانح، ۽ لاگ شامل آهن.",
        "savedprefs": "توهان جون ترجيحات سانڍجي چڪيون آهن.",
        "savedrights": "{{GENDER:$1|$1}} نالي يوزر جا حق سانڇجي چڪا آهن.",
        "timezonelegend": "اوقاتي زون:",
        "localtime": "مقامي وقت:",
+       "timezoneuseserverdefault": "وڪي عدم پيروي استعمال ڪريو ($1)",
        "servertime": "سَروَر پٽاندر وقت:",
        "guesstimezone": "جھانگُوءَ مان ڀريو",
        "timezoneregion-africa": "آفريڪا",
        "allowemail": "ٻين يُوزرس کان ايندڙ برق ٽپال بحال ڪريو",
        "prefs-searchoptions": "ڳولا",
        "prefs-namespaces": "نانءُپولار",
+       "default": "ڏنل",
        "prefs-files": "فائيلس",
        "prefs-emailconfirm-label": "برق ٽپال جي خاطري:",
        "youremail": "برق ٽپال:",
        "prefs-info": "بنيادي ڄاڻ",
        "prefs-i18n": "بين‌الاقوامڪاري",
        "prefs-signature": "صحيح",
+       "prefs-dateformat": "تاريخ جو طرز",
        "prefs-advancedediting": "عمومي چارا",
        "prefs-editor": "ايڊيٽر",
        "prefs-preview": "پيش نگاهہ",
        "email-address-validity-valid": "برق ٽپال پتو قابل ڪار ڏسجي ٿو.",
        "email-address-validity-invalid": "قابل ڪار برق ٽپال پتو ڄاڻايو",
        "userrights": "يُوزر حقن جو بندوبست",
+       "userrights-lookup-user": "يوزر گروپَ سنڀاليو",
        "userrights-user-editname": "يُوزرنانءُ ڄاڻايو:",
+       "editusergroup": "يوزر گروپَ سنواريو",
+       "userrights-editusergroup": "يوزر گروپَ سنواريو",
+       "saveusergroups": "يوزر گروپَ سنڀاليو",
        "userrights-groupsmember": "برڪن:",
        "userrights-groupsmember-auto": "رڪن واجبي:",
        "userrights-reason": "سبب:",
        "userrights-no-interwiki": "توهان کي ٻين وڪيز تي يُوزر حقن ۾ ترميم ڪرڻ جو حق حاصل نہ آهي.",
        "userrights-nodatabase": "اعداخانو $1 يا تہ وجود نہ ٿو رکي يا تہ اهو مقامي اعدادخانو نہ آهي.",
+       "userrights-notallowed": "توهان کي يوزر جا حق شامل يا هٽائڻ جي اجازت نه آهي.",
+       "userrights-changeable-col": "گروپَ جيڪي توهان تبديل ڪري سگھو ٿا",
+       "userrights-unchangeable-col": "گروپَ جيڪي توهان تبديل نٿا ڪري سگھو",
        "group": "گروپ:",
        "group-user": "يوزرس",
        "group-autoconfirmed": "خودبخود پڪ ڪيل يوزرس",
        "right-movefile": "فائيل چوريو",
        "right-upload": "فائيل چاڙهيو",
        "right-upload_by_url": "ڪنهن يُو آر ايل تان فائيل چاڙهيو",
+       "right-writeapi": "اي پر آءِ لکڻ جو استعمال",
        "right-delete": "صفحا ڊاهيو",
        "right-bigdelete": "ڊگھيون سوانح رکندڙ صفحا ڊاهيو",
        "right-browsearchive": "ڊاٺل صفحا ڳوليو",
        "right-undelete": "ڪو صفحو اڻڊاهيو",
+       "right-unblockself": "ڪنهن تان بندش ختم ڪريو",
        "right-editinterface": "يُوزر باهمرُو کي سنواريو",
        "right-viewmywatchlist": "پنهنجي ٽيٽ لسٽ ڏسو",
        "right-editmyoptions": "پنهنجون ترجيحات سنواريو",
        "action-import": "ٻي ڪنهن وڪي کان صفحا درآمد ڪريو",
        "action-importupload": "ڪو فائيل چاڙهي صفحا درآمديو",
        "action-mergehistory": "هن صفحي جي سوانح ضم ڪريو",
+       "action-userrights": "سڀ يوزر حق ترميم ڪريو",
+       "action-userrights-interwiki": "ٻين وڪيز جي يوزرس جا حق ترميم ڪريو",
        "action-siteadmin": "اعدادخاني کي بند ڪريو يا کوليو",
        "action-sendemail": "برق ٽپال اماڻيو",
        "action-editmywatchlist": "پنهنجي ٽيٽ فهرست سنواريو",
        "action-viewmywatchlist": "پنهنجي ٽيٽ فهرست ڏسو",
+       "action-viewmyprivateinfo": "پنهنجي ذاتي معلومات ڏسو",
+       "action-editmyprivateinfo": "پنهنجي ذاتي معلومات سنواريو",
        "nchanges": "$1 {{PLURAL:$1|تبديلي|تبديليون}}",
        "enhancedrc-history": "سوانح",
        "recentchanges": "تازيون تبديليون",
        "filename": "فائيل نانءُ",
        "filedesc": "خلاصو",
        "fileuploadsummary": "خلاصو:",
+       "filereuploadsummary": "فائيل تبديليون:",
+       "filesource": "ذريعو:",
        "filename-tooshort": "فائيل نانءَُ هيڪاندو ننڍو آهي.",
+       "uploadwarning": "چاڙھ جو چتاءُ",
        "savefile": "فائيل سانڍيو",
+       "uploaddisabled": "چاڙھ ناقابلِ ڪار بڻيل.",
        "upload-scripted-pi-callback": "ن فائيل کي اپلوڊ نه ٿو ڪري سگهي جنهن ۾ ايڪس ايم ايل اسٽائيل شيٽ جون پراسيسنگ هدايتون شامل هجن.",
        "uploaded-script-svg": "اسڪرپٽ جوڳو ايليمينٽ ”$1” مليو آهي، اپلوڊ ٿيل ايس وي جي فائيل ۾.",
        "uploaded-hostile-svg": "اپلوڊ ٿيل ايس وي جي فائيل جو غير محفوظ سي ايس ايس ۾ اسٽائيل ايلمينٽ مليو",
        "uploaded-remote-url-svg": "ايس وي جي جيڪا سيٽ ڪري ٿي ڪنهن اسٽائيل وصف  رموٽ يو آر ايل سان  بلاڪ ٿيل آهي.\n <code>$1=\"$2\"</code> اپلوڊ ٿيل ايس وي جي فائيل ۾ مليو",
        "uploaded-image-filter-svg": "هن يو آر ايل سان <code>&lt;$1 $2=\"$3\"&gt;</code> اميج فلٽر مليو آهي، اپلوڊ ٿيل ايس وي جي فائيل ۾،",
        "uploadvirus": "هن فائيل ۾ وائرس آهي! \nتفصيل: $1",
+       "upload-source": "ذريعي جو فائيل",
+       "sourcefilename": "ذريعي جي فائيل جو نالو:",
        "upload-description": "فائيل جي تشريح",
+       "upload-options": "چاڙھ جا چارا",
        "watchthisupload": "هيءُ فائيل ٽيٽيو",
        "upload-success-subj": "چاڙهہ ڪامياب",
+       "upload-failure-subj": "چاڙھ جو مسئلو",
+       "upload-warning-subj": "چاڙھ جو چتاءُ",
        "upload-file-error": "اندروني چُڪَ",
        "upload-dialog-title": "فائيل چاڙهيو",
        "upload-dialog-button-cancel": "رد",
+       "upload-dialog-button-done": "ٿي ويو",
        "upload-dialog-button-save": "سانڍيو",
        "upload-dialog-button-upload": "چاڙهيو",
        "upload-form-label-select-file": "فائيل چونڊيو",
        "foreign-structured-upload-form-label-own-work": "هيءُ منهنجو پنهنجو ڪم آهي.",
        "foreign-structured-upload-form-label-infoform-categories": "زمرا",
        "foreign-structured-upload-form-label-infoform-date": "تاريخ",
+       "backend-fail-notexists": "فائيل ''$1'' وجود نٿو رکي.",
+       "backend-fail-delete": "\"$1\" فائيل ڊهي نہ سگھيو.",
+       "backend-fail-alreadyexists": "\"$1\" فائيل اڳ ئي وجود رکي ٿو.",
+       "backend-fail-copy": "فائيل \"$1\" کي \"$2\" ڏانهن نقل نه ڪري سگھيو.",
+       "backend-fail-move": "فائيل \"$1\" کي \"$2\" ڏانهن چوري نه سگھيو.",
+       "backend-fail-opentemp": "عارضي فائيئ کولي نه سگھيو.",
+       "backend-fail-read": "فائيل \"$1\" کي پڙهي نه سگھيو.",
        "license-header": "لائيسنسڪاري",
        "listfiles-delete": "ڊاهيو",
        "imgfile": "فائيل",
        "listfiles": "فائيل فهرست",
+       "listfiles_date": "تاريخ",
        "listfiles_name": "نالو",
        "listfiles_user": "يُوزر",
        "listfiles_size": "ماپَ",
        "listfiles_description": "تشريح",
+       "listfiles_count": "ورزن",
+       "listfiles-latestversion": "هاڻوڪو ورزن",
        "listfiles-latestversion-yes": "ها",
        "listfiles-latestversion-no": "نہ",
        "file-anchor-link": "فائيل",
        "filehist-current": "حاليہ",
        "filehist-datetime": "تاريخ/وقت",
        "filehist-thumb": "آڱوٺي ننهن",
+       "filehist-thumbtext": "$1 جي نظرثاني لاءِ تصويري نشان",
        "filehist-user": "يُوزر",
        "filehist-dimensions": "ماپَ",
        "filehist-filesize": "فائيل سائيز",
        "linkstoimage": "هن فائيل سان {{PLURAL:$1|هيٺيون صفحو ڳنڍيل آهي |$1 هيٺيان صفحا ڳنڍيل آهن}}:",
        "nolinkstoimage": "هن فائيل سان ڪو بہ صفحو ڳنڍيل ناهي.",
        "sharedupload": "هيءَ فائيل $1 کان آهي ۽ ان کي ٻيون رٿائون به استعمال ڪري سگھن ٿيون.",
+       "sharedupload-desc-here": "هي فائيل $1 مان آهي ۽ ٻين رٿائن پاران پڻ استعمال ٿي سگهي ٿو. تشريح انجي [[$2 جو تشريحي صفحو]] هيٺان ڏنل آهي.",
        "uploadnewversion-linktext": "هن فائيل جو نئون پرت چاڙهيو",
        "shared-repo-from": "$1 کان",
        "upload-disallowed-here": "توهان هن فائيل مٿان لکي نہ ٿا سگھو.",
        "listusers-noresult": "ڪو بہ يُوزر نہ لڌو",
        "listusers-blocked": "(بندشيل)",
        "activeusers": "سرگرم يُوزرس جي فهرست",
+       "activeusers-hidebots": "بوٽس لڪايو",
        "activeusers-hidesysops": "منتظمن کي لڪايو",
        "activeusers-noresult": "ڪي بہ يُوزرس نہ لڌا.",
+       "listgrouprights": "يوزر گروپ جا حق",
        "listgrouprights-group": "گروهہ:",
        "listgrouprights-rights": "حق",
        "listgrouprights-members": "(رڪنن جي لسٽ)",
+       "listgrouprights-addgroup-all": "سڀ گروپَ شامل ڪريو",
+       "listgrouprights-removegroup-all": "سڀ گروپ هٽايو",
        "listgrouprights-namespaceprotection-namespace": "نانءُ پولار:",
+       "trackingcategories-name": "پيغام جو نالو",
+       "trackingcategories-nodesc": "ڪا به تشريح موجود نه آهي.",
+       "trackingcategories-disabled": "زمرو ناقابلِ ڪار بڻايل آهي.",
        "emailuser": "هن يوزر کي برق ٽپال اماڻيو",
        "emailuser-title-notarget": "يُوزر ڏانهن برق ٽپال اماڻيو",
+       "usermaildisabled": "يوزر ايميل ناقابلِ ڪار بڻيل",
+       "usermaildisabledtext": "توهان هن وڪي تي ٻين يوزرس ڏانهن ايميل نٿا موڪلي سگھو",
        "noemailtitle": "برق ٽپال پتو نامعلوم",
        "noemailtext": "هن يُوزر ڪو بہ قابل ڪار برق ٽپال پتو نہ ڄاڻايو آهي.",
        "emailusername": "يُوزرنانءُ:",
        "watchlist-details": "{{PLURAL:$1|$1 صفحو|$1 صفحا}} توهان جي ٽيٽ فهرست، ڳالھ ٻولھ جا صفحا الڳ شمار نٿا ٿين.",
        "wlshowlast": "گذريل $1 ڪلاڪ $2 ڏينهن ڏيکاريو",
        "watchlistall2": "سڀ",
+       "watchlist-hide": "لڪايو",
        "wlshowtime": "آخري ڏيکاريو",
        "wlshowhideminor": "معمولي ترميم",
+       "wlshowhidebots": "بوٽس",
        "wlshowhideliu": "کاتيدار يُوزرس",
        "wlshowhideanons": "گمنام يُوزرس",
        "wlshowhidemine": "منهنجون ترميمون",
+       "watchlist-options": "زيرِ نظر فهرست جا چارا",
        "watching": "ٽيٽيندي...",
        "unwatching": "اڻ ٽيٽيندي...",
+       "enotif_reset": "سڀ گھميل صفحن تي نشان لڳايو",
        "enotif_impersonal_salutation": "{{SITENAME}} يُوزر",
+       "enotif_lastdiff": "هي تبديلي ڏسڻ لاءِ $1 ڏسو",
        "enotif_anon_editor": "گمنام يُوزر $1",
        "created": "ٺهي چڪو",
        "changed": "تبديل ٿي ويو",
        "historywarning": "<strong>خبردار:</strong> جيڪو صفحو توهان ڊاهڻ وڃي رهيا آهيو ان ساڻ هڪ تاريخ آهي $1 {{PLURAL:$1|revision|revisions}}:",
        "confirmdeletetext": "توهان هڪ صفحي کي ان جي سموري سوانح سميت ڊاهڻ وارا آهيو. مهرباني ڪري پڪ ڪندا ته توهان اهو ئي ڪرڻ گھرو ٿا، ۽ اهو ته توهان ان جي نتيجن کان واقف آهيو، ۽ اهو پڻ ته توهان اهو ڪم [[{{MediaWiki:Policy-url}}|پاليسي]]ءَ مطابق ڪري رهيا آهيو.",
        "actioncomplete": "ڪم پُورو",
+       "actionfailed": "عمل ناڪام",
        "deletedtext": "\"$1\" ڊهي چڪو آهي.\nتازو ڊاٺل صفحن جي فهرست لاءِ $2 ڏسندا.",
        "dellogpage": "ڊاٺ لاگ",
        "deletionlog": "ڊاٺ لاگ",
        "rollbacklinkcount": "اڻ ڪريو $1 {{PLURAL:$1|ترميم|ترميمون}}",
        "changecontentmodel-title-label": "صفحي جو عنوان",
        "changecontentmodel-reason-label": "سبب:",
+       "logentry-contentmodel-change-revertlink": "واپس ورايو",
+       "logentry-contentmodel-change-revert": "واپس ورايو",
        "protectlogpage": "تحفظ لاگ",
+       "protectedarticle": "محفوظ ٿيل \"[[$1]]\"",
        "prot_1movedto2": "[[$1]] کي چوري [[$2]] تي رکيو ويو",
        "protect-legend": "تحفظڻ جي پڪ ڪريو",
        "protectcomment": "سبب:",
        "protect-level-autoconfirmed": "خودڪار نموني پڪ ڪيل يوزرس کي اجازت ڏيو",
        "protect-level-sysop": "صرف منتظمين کي اجازت ڏيو",
        "protect-summary-cascade": "تحفظ در تحفظ",
+       "protect-expiry-indefinite": "لامحدود",
        "protect-cascade": "هن صفحي ۾ شامل صفحن کي تحفظيو (تحفظ در تحفظ)",
        "protect-cantedit": "توهان هن صفحي جي تحفظاتي سطح نٿا بدلائي سگھو، ڇاڪاڻ ته توهان ان کي سنوارڻ جي اجازت نٿا رکو.",
+       "protect-othertime": "ٻيو وقت:",
+       "protect-othertime-op": "ٻيو وقت",
+       "protect-existing-expiry": "موجوده پڄاڻي جو وقت: $3, $2",
+       "protect-existing-expiry-infinity": "موجوده پڄاڻي جو وقت: لامحدود",
        "protect-otherreason-op": "ٻيو سبب",
        "protect-expiry-options": "1 ڪلاڪ:1 hour,1 ڏينهن:1 day,1 هفتو:1 week,2 هفتو:2 weeks,1 مهينا:1 month,3 مهينا:3 months,6 مهينا:6 months,1 سال:1 year,اڻ کٽ:infinite",
        "restriction-type": "اجازتنامو:",
        "undeletebtn": "بحاليو",
        "undeleteviewlink": "ڏسو",
        "undeletecomment": "سبب:",
+       "undelete-search-title": "ڊاٺل صفحا ڳوليو",
+       "undelete-search-box": "ڊاٺل صفحا ڳوليو",
+       "undelete-search-prefix": "سان شروع ٿيندڙ صفحا ڏيکاريو:",
        "undelete-search-submit": "ڳوليو",
        "undelete-error-short": "هيءُ فائيل اڻڊاهيندي چُڪَ ٿي آهي: $1",
        "undelete-show-file-submit": "ها",
        "namespace": "نانءُ پولار:",
        "invert": "چونڊ ابتيو",
+       "tooltip-invert": "هن دٻي تي نشان لڳايو صحفن ۾ تبديليون لڪائڻ لاءِ چونڊيل نيم اسپيس مان (۽ لاڳاپيل نيم اسپيس جيڪڏهن نشان لڳل)",
        "namespace_association": "منسلڪ نانءُپولار",
        "blanknamespace": "(مُک)",
        "contributions": "{{GENDER:$1|يوزر}} جون ڀاڱيداريون",
        "contributions-title": "يُوزر ڀاڱيداريون براءِ $1",
        "mycontris": "ڀاڱيداريون",
+       "anoncontribs": "ڀاڱيداريون",
        "contribsub2": "{{GENDER:$3|$1}} ($2) لاءِ",
+       "contributions-userdoesnotexist": "يُوزر کاتو \"$1\" درج ٿيل نہ آهي.",
        "uctop": "(هاڻوڪو)",
        "month": "مهينو (۽ اڳوڻيون):",
        "year": "سال (۽ اڳوڻيون):",
+       "sp-contributions-newbies": "صرف نون کاتن جون ڀاڱيداريون ڏيکاريو",
        "sp-contributions-newbies-sub": "نون کاتن لاءِ",
+       "sp-contributions-newbies-title": "نون کاتن جي لاءِ يوزر جون ڀاڱيداريون",
        "sp-contributions-blocklog": "بنسش لاگ",
+       "sp-contributions-deleted": "يُوزر جون ڊاٺل ڀاڱيداريون",
+       "sp-contributions-uploads": "چاڙھَ",
+       "sp-contributions-logs": "لاگس",
        "sp-contributions-talk": "ڳالھہ",
+       "sp-contributions-userrights": "يُوزر حقن جي سنڀال",
+       "sp-contributions-search": "ڀاڱيدارين لاءِ ڳولا ڪريو",
+       "sp-contributions-username": "آءِپي پتو يا يوزرنانءُ:",
        "sp-contributions-submit": "ڳوليو",
        "whatlinkshere": "هتان ڇا ڳنڍيل آهي",
        "whatlinkshere-title": "$1 سان ڳنڍيل صفحا",
        "blockip-legend": "يُوزر کي بندشيو",
        "ipbexpiry": "اختتام:",
        "ipbreason": "سبب:",
+       "ipbother": "ٻيو وقت:",
        "ipboptions": "2 ڪلاڪ:2 hours,1 ڏينهن:1 day,3 days:3 days,1 هفتو:1 week,2 weeks:2 weeks,1 مهينا:1 month,3 مهينا:3 months,6 مهينا:6 months,1 سال:1 year,اڻ کٽ:infinite",
        "ipb-confirm": "بندش جي پڪ ڪريو",
        "badipaddress": "ناقابلڪار آءِ پي پتو",
        "ipblocklist": "بندشيل يوزرس",
        "blocklist-timestamp": "اوقاتي مهر",
        "blocklist-target": "هدف",
+       "blocklist-expiry": "اختتام:",
        "blocklist-reason": "سبب",
        "ipblocklist-submit": "ڳوليو",
        "ipblocklist-localblock": "مقامي بندش",
index af3d8ac..9e6c226 100644 (file)
        "morenotlisted": "Seznam ni popoln.",
        "mypage": "Stran",
        "mytalk": "Pogovor",
-       "anontalk": "Pogovorna stran IP-naslova",
+       "anontalk": "Pogovorna stran",
        "navigation": "Navigacija",
        "and": "&#32;in",
        "qbfind": "Poišči",
        "contributions": "{{GENDER:$1|Uporabnikovi|Uporabničini}} prispevki",
        "contributions-title": "Prispevki uporabnika $1",
        "mycontris": "Prispevki",
+       "anoncontribs": "Prispevki",
        "contribsub2": "Za {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Uporabniški račun »$1« ni registriran.",
        "nocontribs": "Ne najdem nobene merilom ustrezajoče spremembe.",
        "tooltip-pt-preferences": "Vaše nastavitve",
        "tooltip-pt-watchlist": "Seznam strani, katerih spremembe spremljate",
        "tooltip-pt-mycontris": "Seznam vaših prispevkov",
+       "tooltip-pt-anoncontribs": "Seznam urejanj s tega IP-naslova",
        "tooltip-pt-login": "Prijava ni obvezna, vendar je zaželena",
        "tooltip-pt-logout": "Odjavite se",
        "tooltip-pt-createaccount": "Predlagamo vam, da ustvarite račun in se prijavite, vendar pa to ni obvezno.",
index ab6b43d..7702e8b 100644 (file)
        "morenotlisted": "Ова листа није комплетна.",
        "mypage": "Страница",
        "mytalk": "Разговор",
-       "anontalk": "Разговор за ову ИП адресу",
+       "anontalk": "Разговор",
        "navigation": "Навигација",
        "and": "&#32;и",
        "qbfind": "Пронађи",
        "contributions": "{{GENDER:$1|Кориснички}} доприноси",
        "contributions-title": "Доприноси {{GENDER:$1|корисника|кориснице}} $1",
        "mycontris": "Доприноси",
+       "anoncontribs": "Доприноси",
        "contribsub2": "За {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Кориснички налог „$1“ није регистрован.",
        "nocontribs": "Нема измена које одговарају наведеним критеријумима.",
        "tooltip-pt-preferences": "Ваша подешавања",
        "tooltip-pt-watchlist": "Списак страница које надгледате",
        "tooltip-pt-mycontris": "Списак ваших доприноса",
+       "tooltip-pt-anoncontribs": "Списак измена направљених са ове IP адресе",
        "tooltip-pt-login": "Препоручујемо вам да се пријавите, иако то није обавезно.",
        "tooltip-pt-logout": "Одјавите се",
        "tooltip-pt-createaccount": "Охрабрујемо вас да отворите налог и пријавите се али то није обавезно",
        "tags-deactivate-not-allowed": "Није могуће деактивирати ознаку „$1“.",
        "tags-deactivate-submit": "Декативирај",
        "tags-edit-title": "Уреди ознаке",
+       "tags-edit-manage-link": "Управљај ознакама",
        "tags-edit-existing-tags": "Постојеће ознаке:",
        "tags-edit-new-tags": "Нове ознаке:",
        "tags-edit-reason": "Разлог:",
        "htmlform-cloner-create": "Додај још",
        "htmlform-cloner-delete": "Уклони",
        "htmlform-cloner-required": "Бар једна вредност је потребна.",
+       "htmlform-title-badnamespace": "[[:$1]] није у именском простору „{{ns:$2}}“.",
        "htmlform-title-not-exists": "$1 не постоји.",
        "htmlform-user-not-exists": "<strong>$1</strong> не постоји.",
        "htmlform-user-not-valid": "<strong>$1</strong> није исправно корисничко име.",
        "special-characters-group-thai": "тајландски",
        "special-characters-group-lao": "лаоски",
        "special-characters-group-khmer": "кмерски",
+       "mw-widgets-dateinput-no-date": "Датум није изабран",
        "mw-widgets-dateinput-placeholder-day": "ГГГГ-ММ-ДД",
        "mw-widgets-dateinput-placeholder-month": "ГГГГ-ММ",
        "mw-widgets-titleinput-description-new-page": "страница још увек не постоји",
index c8e3e54..ec82bcb 100644 (file)
        "morenotlisted": "Ova lista nije kompletna.",
        "mypage": "Stranica",
        "mytalk": "Razgovor",
-       "anontalk": "Razgovor za ovu IP adresu",
+       "anontalk": "Razgovor",
        "navigation": "Navigacija",
        "and": "&#32;i",
        "qbfind": "Pronađi",
index c12640e..ad56e39 100644 (file)
        "morenotlisted": "Denna lista är inte fullständig.",
        "mypage": "Sida",
        "mytalk": "Diskussion",
-       "anontalk": "Diskussionssida för denna IP-adress",
+       "anontalk": "Diskussion",
        "navigation": "Navigering",
        "and": "&#32;och",
        "qbfind": "Hitta",
        "contributions": "{{GENDER:$1|Användarbidrag}}",
        "contributions-title": "Bidrag av $1",
        "mycontris": "Bidrag",
+       "anoncontribs": "Bidrag",
        "contribsub2": "För {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Användarkontot \"$1\" är inte registrerat.",
        "nocontribs": "Inga ändringar som motsvarar dessa kriterier hittades.",
        "tooltip-pt-preferences": "Dina inställningar",
        "tooltip-pt-watchlist": "Listan över sidor du bevakar för ändringar",
        "tooltip-pt-mycontris": "Lista över dina bidrag",
+       "tooltip-pt-anoncontribs": "En lista över redigeringar från denna IP-adress",
        "tooltip-pt-login": "Du uppmuntras att logga in, men det är inget krav",
        "tooltip-pt-logout": "Logga ut",
        "tooltip-pt-createaccount": "Du uppmuntras att skapa ett konto och logga in, men det är inte obligatoriskt",
        "filedelete-archive-read-only": "Webbservern kan inte skriva till arkivkatalogen \"$1\".",
        "previousdiff": "← Äldre redigering",
        "nextdiff": "Nyare redigering →",
-       "mediawarning": "'''Varning''': Den här filen kan innehålla elak kod.\nOm du kör den kan din dator skadas.",
+       "mediawarning": "'''Varning''': Den här filen kan innehålla skadlig kod.\nOm du kör den kan din dator skadas.",
        "imagemaxsize": "Begränsa bilders storlek:<br />''(för filbeskrivningssidor)''",
        "thumbsize": "Storlek på minibild:",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|sida|sidor}}",
index af67776..8cd5662 100644 (file)
        "createacct-emailrequired": "Электрон почта юлламагыз",
        "createacct-emailoptional": "Электрон почта юлламагыз (мәҗбүри түгел)",
        "createacct-email-ph": "Электрон почта юлламагызны языгыз",
+       "createacct-another-email-ph": "Электрон почта юлламагызны кертегез",
        "createaccountmail": "электрон почта аша",
+       "createacct-realname": "Чын исем (мәҗбүри түгел)",
        "createaccountreason": "Сәбәп:",
+       "createacct-reason": "Сәбәп",
+       "createacct-reason-ph": "Нигә сез яңа зисап язмасы төзисез",
        "createacct-submit": "Хисап язмасы төзү",
+       "createacct-another-submit": "Хисап язмасын төзү",
        "createacct-benefit-heading": "{{SITENAME}} — сезнең шикелле кешеләрнең хезмәте.",
        "createacct-benefit-body1": "{{PLURAL:$1|төзәтмә}}",
        "createacct-benefit-body2": "{{PLURAL:$1|мәкалә}}",
        "rcshowhideminor-show": "күрсәт",
        "rcshowhideminor-hide": "яшер",
        "rcshowhidebots": "ботларны $1",
-       "rcshowhidebots-show": "күрсәт",
+       "rcshowhidebots-show": "Ð\9aүрсәт",
        "rcshowhidebots-hide": "яшер",
        "rcshowhideliu": "теркәлгән кулланучыларны $1",
        "rcshowhideliu-show": "күрсәт",
        "contributions": "{{GENDER:$1|Кулланучының}} кертеме",
        "contributions-title": "$1 исемле кулланучының кертеме",
        "mycontris": "Кертем",
+       "anoncontribs": "Кертем",
        "contribsub2": "Кертем {{GENDER:$3|$1}} ($2)",
        "uctop": "(хәзерге)",
        "month": "Айдан башлап (һәм элегрәк):",
index ccdd81d..cf80d0b 100644 (file)
        "morenotlisted": "本列表不完整。",
        "mypage": "页面",
        "mytalk": "讨论",
-       "anontalk": "该IPå\9c°å\9d\80ç\9a\84讨论",
+       "anontalk": "讨论",
        "navigation": "导航",
        "and": "和",
        "qbfind": "查找",
        "contributions": "{{GENDER:$1|用户}}贡献",
        "contributions-title": "$1的用户贡献",
        "mycontris": "贡献",
+       "anoncontribs": "贡献",
        "contribsub2": "{{GENDER:$3|$1}}的贡献($2)",
        "contributions-userdoesnotexist": "用户“$1”尚未注册。",
        "nocontribs": "没有找到匹配这些规则的更改。",
        "tooltip-pt-preferences": "你的设置",
        "tooltip-pt-watchlist": "你正在监视更改的页面的列表",
        "tooltip-pt-mycontris": "你的贡献的列表",
+       "tooltip-pt-anoncontribs": "来自此IP地址的编辑列表",
        "tooltip-pt-login": "我们鼓励您登录;然而,这不是强制性的",
        "tooltip-pt-logout": "退出登录",
        "tooltip-pt-createaccount": "建议您创建一个账户并登录,但这不是强制的",
index 0eb5118..aba5ce8 100644 (file)
@@ -1834,6 +1834,9 @@ return array(
                        'mediawiki.util',
                ),
        ),
+       'mediawiki.special.watchlist' => array(
+               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
+       ),
        'mediawiki.special.javaScriptTest' => array(
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js',
                'messages' => array_merge( Skin::getSkinNameMessages(), array(
index caf6dab..d3b74f2 100644 (file)
@@ -36,6 +36,7 @@ return call_user_func( function () {
        $modules['oojs-ui'] = array(
                'scripts' => array(
                        'resources/lib/oojs-ui/oojs-ui.js',
+                       'resources/src/oojs-ui-local.js',
                ),
                'skinScripts' => array_combine(
                        array_keys( $themes ),
@@ -51,6 +52,7 @@ return call_user_func( function () {
                        'oojs-ui.styles.icons',
                        'oojs-ui.styles.indicators',
                        'oojs-ui.styles.textures',
+                       'mediawiki.language',
                ),
                'messages' => array(
                        'ooui-dialog-message-accept',
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.js b/resources/src/mediawiki.special/mediawiki.special.watchlist.js
new file mode 100644 (file)
index 0000000..a35f4d1
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * JavaScript for Special:Watchlist
+ *
+ * This script is only loaded, if the user opt-in a setting in Special:Preferences,
+ * that the watchlist should be automatically reloaded, when a filter option is
+ * changed in the header form.
+ */
+jQuery( function ( $ ) {
+       // add a listener on all form elements in the header form
+       $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
+               // submit the form, when one of the input fields was changed
+               $( '#mw-watchlist-form' ).submit();
+       } );
+
+} );
diff --git a/resources/src/oojs-ui-local.js b/resources/src/oojs-ui-local.js
new file mode 100644 (file)
index 0000000..84ec92d
--- /dev/null
@@ -0,0 +1,5 @@
+// Connect OOjs UI to MediaWiki's localisation system
+( function ( mw ) {
+       OO.ui.getUserLanguages = mw.language.getFallbackLanguageChain;
+       OO.ui.msg = mw.msg;
+}( mediaWiki ) );
diff --git a/tests/phpunit/includes/UserArrayFromResultTest.php b/tests/phpunit/includes/UserArrayFromResultTest.php
deleted file mode 100644 (file)
index 469ad29..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-/**
- * @author Adam Shorland
- * @covers UserArrayFromResult
- */
-class UserArrayFromResultTest extends MediaWikiTestCase {
-
-       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
-               $resultWrapper = $this->getMockBuilder( 'ResultWrapper' )
-                       ->disableOriginalConstructor();
-
-               $resultWrapper = $resultWrapper->getMock();
-               $resultWrapper->expects( $this->atLeastOnce() )
-                       ->method( 'current' )
-                       ->will( $this->returnValue( $row ) );
-               $resultWrapper->expects( $this->any() )
-                       ->method( 'numRows' )
-                       ->will( $this->returnValue( $numRows ) );
-
-               return $resultWrapper;
-       }
-
-       private function getRowWithUsername( $username = 'fooUser' ) {
-               $row = new stdClass();
-               $row->user_name = $username;
-               return $row;
-       }
-
-       private function getUserArrayFromResult( $resultWrapper ) {
-               return new UserArrayFromResult( $resultWrapper );
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithFalseRow() {
-               $row = false;
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = $this->getUserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertEquals( $row, $object->current );
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithRow() {
-               $username = 'addshore';
-               $row = $this->getRowWithUsername( $username );
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = $this->getUserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertInstanceOf( 'User', $object->current );
-               $this->assertEquals( $username, $object->current->mName );
-       }
-
-       public static function provideNumberOfRows() {
-               return array(
-                       array( 0 ),
-                       array( 1 ),
-                       array( 122 ),
-               );
-       }
-
-       /**
-        * @dataProvider provideNumberOfRows
-        * @covers UserArrayFromResult::count
-        */
-       public function testCountWithVaryingValues( $numRows ) {
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper(
-                       $this->getRowWithUsername(),
-                       $numRows
-               ) );
-               $this->assertEquals( $numRows, $object->count() );
-       }
-
-       /**
-        * @covers UserArrayFromResult::current
-        */
-       public function testCurrentAfterConstruction() {
-               $username = 'addshore';
-               $userRow = $this->getRowWithUsername( $username );
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
-               $this->assertInstanceOf( 'User', $object->current() );
-               $this->assertEquals( $username, $object->current()->mName );
-       }
-
-       public function provideTestValid() {
-               return array(
-                       array( $this->getRowWithUsername(), true ),
-                       array( false, false ),
-               );
-       }
-
-       /**
-        * @dataProvider provideTestValid
-        * @covers UserArrayFromResult::valid
-        */
-       public function testValid( $input, $expected ) {
-               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $input ) );
-               $this->assertEquals( $expected, $object->valid() );
-       }
-
-       // @todo unit test for key()
-       // @todo unit test for next()
-       // @todo unit test for rewind()
-}
diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php
deleted file mode 100644 (file)
index 4c6f083..0000000
+++ /dev/null
@@ -1,534 +0,0 @@
-<?php
-
-define( 'NS_UNITTEST', 5600 );
-define( 'NS_UNITTEST_TALK', 5601 );
-
-/**
- * @group Database
- */
-class UserTest extends MediaWikiTestCase {
-       /**
-        * @var User
-        */
-       protected $user;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->setMwGlobals( array(
-                       'wgGroupPermissions' => array(),
-                       'wgRevokePermissions' => array(),
-               ) );
-
-               $this->setUpPermissionGlobals();
-
-               $this->user = new User;
-               $this->user->addGroup( 'unittesters' );
-       }
-
-       private function setUpPermissionGlobals() {
-               global $wgGroupPermissions, $wgRevokePermissions;
-
-               # Data for regular $wgGroupPermissions test
-               $wgGroupPermissions['unittesters'] = array(
-                       'test' => true,
-                       'runtest' => true,
-                       'writetest' => false,
-                       'nukeworld' => false,
-               );
-               $wgGroupPermissions['testwriters'] = array(
-                       'test' => true,
-                       'writetest' => true,
-                       'modifytest' => true,
-               );
-
-               # Data for regular $wgRevokePermissions test
-               $wgRevokePermissions['formertesters'] = array(
-                       'runtest' => true,
-               );
-
-               # For the options test
-               $wgGroupPermissions['*'] = array(
-                       'editmyoptions' => true,
-               );
-       }
-
-       /**
-        * @covers User::getGroupPermissions
-        */
-       public function testGroupPermissions() {
-               $rights = User::getGroupPermissions( array( 'unittesters' ) );
-               $this->assertContains( 'runtest', $rights );
-               $this->assertNotContains( 'writetest', $rights );
-               $this->assertNotContains( 'modifytest', $rights );
-               $this->assertNotContains( 'nukeworld', $rights );
-
-               $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) );
-               $this->assertContains( 'runtest', $rights );
-               $this->assertContains( 'writetest', $rights );
-               $this->assertContains( 'modifytest', $rights );
-               $this->assertNotContains( 'nukeworld', $rights );
-       }
-
-       /**
-        * @covers User::getGroupPermissions
-        */
-       public function testRevokePermissions() {
-               $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) );
-               $this->assertNotContains( 'runtest', $rights );
-               $this->assertNotContains( 'writetest', $rights );
-               $this->assertNotContains( 'modifytest', $rights );
-               $this->assertNotContains( 'nukeworld', $rights );
-       }
-
-       /**
-        * @covers User::getRights
-        */
-       public function testUserPermissions() {
-               $rights = $this->user->getRights();
-               $this->assertContains( 'runtest', $rights );
-               $this->assertNotContains( 'writetest', $rights );
-               $this->assertNotContains( 'modifytest', $rights );
-               $this->assertNotContains( 'nukeworld', $rights );
-       }
-
-       /**
-        * @dataProvider provideGetGroupsWithPermission
-        * @covers User::getGroupsWithPermission
-        */
-       public function testGetGroupsWithPermission( $expected, $right ) {
-               $result = User::getGroupsWithPermission( $right );
-               sort( $result );
-               sort( $expected );
-
-               $this->assertEquals( $expected, $result, "Groups with permission $right" );
-       }
-
-       public static function provideGetGroupsWithPermission() {
-               return array(
-                       array(
-                               array( 'unittesters', 'testwriters' ),
-                               'test'
-                       ),
-                       array(
-                               array( 'unittesters' ),
-                               'runtest'
-                       ),
-                       array(
-                               array( 'testwriters' ),
-                               'writetest'
-                       ),
-                       array(
-                               array( 'testwriters' ),
-                               'modifytest'
-                       ),
-               );
-       }
-
-       /**
-        * @dataProvider provideIPs
-        * @covers User::isIP
-        */
-       public function testIsIP( $value, $result, $message ) {
-               $this->assertEquals( $this->user->isIP( $value ), $result, $message );
-       }
-
-       public static function provideIPs() {
-               return array(
-                       array( '', false, 'Empty string' ),
-                       array( ' ', false, 'Blank space' ),
-                       array( '10.0.0.0', true, 'IPv4 private 10/8' ),
-                       array( '10.255.255.255', true, 'IPv4 private 10/8' ),
-                       array( '192.168.1.1', true, 'IPv4 private 192.168/16' ),
-                       array( '203.0.113.0', true, 'IPv4 example' ),
-                       array( '2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff', true, 'IPv6 example' ),
-                       // Not valid IPs but classified as such by MediaWiki for negated asserting
-                       // of whether this might be the identifier of a logged-out user or whether
-                       // to allow usernames like it.
-                       array( '300.300.300.300', true, 'Looks too much like an IPv4 address' ),
-                       array( '203.0.113.xxx', true, 'Assigned by UseMod to cloaked logged-out users' ),
-               );
-       }
-
-       /**
-        * @dataProvider provideUserNames
-        * @covers User::isValidUserName
-        */
-       public function testIsValidUserName( $username, $result, $message ) {
-               $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message );
-       }
-
-       public static function provideUserNames() {
-               return array(
-                       array( '', false, 'Empty string' ),
-                       array( ' ', false, 'Blank space' ),
-                       array( 'abcd', false, 'Starts with small letter' ),
-                       array( 'Ab/cd', false, 'Contains slash' ),
-                       array( 'Ab cd', true, 'Whitespace' ),
-                       array( '192.168.1.1', false, 'IP' ),
-                       array( 'User:Abcd', false, 'Reserved Namespace' ),
-                       array( '12abcd232', true, 'Starts with Numbers' ),
-                       array( '?abcd', true, 'Start with ? mark' ),
-                       array( '#abcd', false, 'Start with #' ),
-                       array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ),
-                       array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ),
-                       array( 'Ab cd', false, ' Ideographic space' ),
-                       array( '300.300.300.300', false, 'Looks too much like an IPv4 address' ),
-                       array( '302.113.311.900', false, 'Looks too much like an IPv4 address' ),
-                       array( '203.0.113.xxx', false, 'Reserved for usage by UseMod for cloaked logged-out users' ),
-               );
-       }
-
-       /**
-        * Test, if for all rights a right- message exist,
-        * which is used on Special:ListGroupRights as help text
-        * Extensions and core
-        */
-       public function testAllRightsWithMessage() {
-               // Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
-               $allRights = User::getAllRights();
-               $allMessageKeys = Language::getMessageKeysFor( 'en' );
-
-               $rightsWithMessage = array();
-               foreach ( $allMessageKeys as $message ) {
-                       // === 0: must be at beginning of string (position 0)
-                       if ( strpos( $message, 'right-' ) === 0 ) {
-                               $rightsWithMessage[] = substr( $message, strlen( 'right-' ) );
-                       }
-               }
-
-               sort( $allRights );
-               sort( $rightsWithMessage );
-
-               $this->assertEquals(
-                       $allRights,
-                       $rightsWithMessage,
-                       'Each user rights (core/extensions) has a corresponding right- message.'
-               );
-       }
-
-       /**
-        * Test User::editCount
-        * @group medium
-        * @covers User::getEditCount
-        */
-       public function testEditCount() {
-               $user = User::newFromName( 'UnitTestUser' );
-
-               if ( !$user->getId() ) {
-                       $user->addToDatabase();
-               }
-
-               // let the user have a few (3) edits
-               $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
-               for ( $i = 0; $i < 3; $i++ ) {
-                       $page->doEdit( (string)$i, 'test', 0, false, $user );
-               }
-
-               $user->clearInstanceCache();
-               $this->assertEquals(
-                       3,
-                       $user->getEditCount(),
-                       'After three edits, the user edit count should be 3'
-               );
-
-               // increase the edit count and clear the cache
-               $user->incEditCount();
-
-               $user->clearInstanceCache();
-               $this->assertEquals(
-                       4,
-                       $user->getEditCount(),
-                       'After increasing the edit count manually, the user edit count should be 4'
-               );
-       }
-
-       /**
-        * Test changing user options.
-        * @covers User::setOption
-        * @covers User::getOption
-        */
-       public function testOptions() {
-               $user = User::newFromName( 'UnitTestUser' );
-
-               if ( !$user->getId() ) {
-                       $user->addToDatabase();
-               }
-
-               $user->setOption( 'userjs-someoption', 'test' );
-               $user->setOption( 'cols', 200 );
-               $user->saveSettings();
-
-               $user = User::newFromName( 'UnitTestUser' );
-               $this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) );
-               $this->assertEquals( 200, $user->getOption( 'cols' ) );
-       }
-
-       /**
-        * Bug 37963
-        * Make sure defaults are loaded when setOption is called.
-        * @covers User::loadOptions
-        */
-       public function testAnonOptions() {
-               global $wgDefaultUserOptions;
-               $this->user->setOption( 'userjs-someoption', 'test' );
-               $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) );
-               $this->assertEquals( 'test', $this->user->getOption( 'userjs-someoption' ) );
-       }
-
-       /**
-        * Test password validity checks. There are 3 checks in core,
-        *      - ensure the password meets the minimal length
-        *      - ensure the password is not the same as the username
-        *      - ensure the username/password combo isn't forbidden
-        * @covers User::checkPasswordValidity()
-        * @covers User::getPasswordValidity()
-        * @covers User::isValidPassword()
-        */
-       public function testCheckPasswordValidity() {
-               $this->setMwGlobals( array(
-                       'wgPasswordPolicy' => array(
-                               'policies' => array(
-                                       'sysop' => array(
-                                               'MinimalPasswordLength' => 8,
-                                               'MinimumPasswordLengthToLogin' => 1,
-                                               'PasswordCannotMatchUsername' => 1,
-                                       ),
-                                       'default' => array(
-                                               'MinimalPasswordLength' => 6,
-                                               'PasswordCannotMatchUsername' => true,
-                                               'PasswordCannotMatchBlacklist' => true,
-                                               'MaximalPasswordLength' => 30,
-                                       ),
-                               ),
-                               'checks' => array(
-                                       'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength',
-                                       'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
-                                       'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
-                                       'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
-                                       'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
-                               ),
-                       ),
-               ) );
-
-               $user = User::newFromName( 'Useruser' );
-               // Sanity
-               $this->assertTrue( $user->isValidPassword( 'Password1234' ) );
-
-               // Minimum length
-               $this->assertFalse( $user->isValidPassword( 'a' ) );
-               $this->assertFalse( $user->checkPasswordValidity( 'a' )->isGood() );
-               $this->assertTrue( $user->checkPasswordValidity( 'a' )->isOK() );
-               $this->assertEquals( 'passwordtooshort', $user->getPasswordValidity( 'a' ) );
-
-               // Maximum length
-               $longPass = str_repeat( 'a', 31 );
-               $this->assertFalse( $user->isValidPassword( $longPass ) );
-               $this->assertFalse( $user->checkPasswordValidity( $longPass )->isGood() );
-               $this->assertFalse( $user->checkPasswordValidity( $longPass )->isOK() );
-               $this->assertEquals( 'passwordtoolong', $user->getPasswordValidity( $longPass ) );
-
-               // Matches username
-               $this->assertFalse( $user->checkPasswordValidity( 'Useruser' )->isGood() );
-               $this->assertTrue( $user->checkPasswordValidity( 'Useruser' )->isOK() );
-               $this->assertEquals( 'password-name-match', $user->getPasswordValidity( 'Useruser' ) );
-
-               // On the forbidden list
-               $this->assertFalse( $user->checkPasswordValidity( 'Passpass' )->isGood() );
-               $this->assertEquals( 'password-login-forbidden', $user->getPasswordValidity( 'Passpass' ) );
-       }
-
-       /**
-        * @covers User::getCanonicalName()
-        * @dataProvider provideGetCanonicalName
-        */
-       public function testGetCanonicalName( $name, $expectedArray, $msg ) {
-               foreach ( $expectedArray as $validate => $expected ) {
-                       $this->assertEquals(
-                               $expected,
-                               User::getCanonicalName( $name, $validate === 'false' ? false : $validate ),
-                               $msg . ' (' . $validate . ')'
-                       );
-               }
-       }
-
-       public static function provideGetCanonicalName() {
-               return array(
-                       array( ' Trailing space ', array( 'creatable' => 'Trailing space' ), 'Trailing spaces' ),
-                       // @todo FIXME: Maybe the creatable name should be 'Talk:Username' or false to reject?
-                       array( 'Talk:Username', array( 'creatable' => 'Username', 'usable' => 'Username',
-                               'valid' => 'Username', 'false' => 'Talk:Username' ), 'Namespace prefix' ),
-                       array( ' name with # hash', array( 'creatable' => false, 'usable' => false ), 'With hash' ),
-                       array( 'Multi  spaces', array( 'creatable' => 'Multi spaces',
-                               'usable' => 'Multi spaces' ), 'Multi spaces' ),
-                       array( 'lowercase', array( 'creatable' => 'Lowercase' ), 'Lowercase' ),
-                       array( 'in[]valid', array( 'creatable' => false, 'usable' => false, 'valid' => false,
-                               'false' => 'In[]valid' ), 'Invalid' ),
-                       array( 'with / slash', array( 'creatable' => false, 'usable' => false, 'valid' => false,
-                               'false' => 'With / slash' ), 'With slash' ),
-               );
-       }
-
-       /**
-        * @covers User::equals
-        */
-       public function testEquals() {
-               $first = User::newFromName( 'EqualUser' );
-               $second = User::newFromName( 'EqualUser' );
-
-               $this->assertTrue( $first->equals( $first ) );
-               $this->assertTrue( $first->equals( $second ) );
-               $this->assertTrue( $second->equals( $first ) );
-
-               $third = User::newFromName( '0' );
-               $fourth = User::newFromName( '000' );
-
-               $this->assertFalse( $third->equals( $fourth ) );
-               $this->assertFalse( $fourth->equals( $third ) );
-
-               // Test users loaded from db with id
-               $user = User::newFromName( 'EqualUnitTestUser' );
-               if ( !$user->getId() ) {
-                       $user->addToDatabase();
-               }
-
-               $id = $user->getId();
-
-               $fifth = User::newFromId( $id );
-               $sixth = User::newFromName( 'EqualUnitTestUser' );
-               $this->assertTrue( $fifth->equals( $sixth ) );
-       }
-
-       /**
-        * @covers User::getId
-        */
-       public function testGetId() {
-               $user = User::newFromName( 'UTSysop' );
-               $this->assertTrue( $user->getId() > 0 );
-
-       }
-
-       /**
-        * @covers User::isLoggedIn
-        * @covers User::isAnon
-        */
-       public function testLoggedIn() {
-               $user = User::newFromName( 'UTSysop' );
-               $this->assertTrue( $user->isLoggedIn() );
-               $this->assertFalse( $user->isAnon() );
-
-               // Non-existent users are perceived as anonymous
-               $user = User::newFromName( 'UTNonexistent' );
-               $this->assertFalse( $user->isLoggedIn() );
-               $this->assertTrue( $user->isAnon() );
-
-               $user = new User;
-               $this->assertFalse( $user->isLoggedIn() );
-               $this->assertTrue( $user->isAnon() );
-       }
-
-       /**
-        * @covers User::checkAndSetTouched
-        */
-       public function testCheckAndSetTouched() {
-               $user = TestingAccessWrapper::newFromObject( User::newFromName( 'UTSysop' ) );
-               $this->assertTrue( $user->isLoggedIn() );
-
-               $touched = $user->getDBTouched();
-               $this->assertTrue(
-                       $user->checkAndSetTouched(), "checkAndSetTouched() succeded" );
-               $this->assertGreaterThan(
-                       $touched, $user->getDBTouched(), "user_touched increased with casOnTouched()" );
-
-               $touched = $user->getDBTouched();
-               $this->assertTrue(
-                       $user->checkAndSetTouched(), "checkAndSetTouched() succeded #2" );
-               $this->assertGreaterThan(
-                       $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
-       }
-
-       public static function setExtendedLoginCookieDataProvider() {
-               $data = array();
-               $now = time();
-
-               $secondsInDay = 86400;
-
-               // Arbitrary durations, in units of days, to ensure it chooses the
-               // right one.  There is a 5-minute grace period (see testSetExtendedLoginCookie)
-               // to work around slow tests, since we're not currently mocking time() for PHP.
-
-               $durationOne = $secondsInDay * 5;
-               $durationTwo = $secondsInDay * 29;
-               $durationThree = $secondsInDay * 17;
-
-               // If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
-               // set cookie is time() + $wgCookieExpiration
-               $data[] = array(
-                       null,
-                       $durationOne,
-                       $now + $durationOne,
-               );
-
-               // If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
-               // set cookie is $now + $wgExtendedLoginCookieExpiration
-               $data[] = array(
-                       $durationTwo,
-                       $durationThree,
-                       $now + $durationTwo,
-               );
-
-               return $data;
-       }
-
-       /**
-        * @dataProvider setExtendedLoginCookieDataProvider
-        * @covers User::getRequest
-        * @covers User::setCookie
-        * @backupGlobals enabled
-        */
-       public function testSetExtendedLoginCookie(
-               $extendedLoginCookieExpiration,
-               $cookieExpiration,
-               $expectedExpiry
-       ) {
-               $this->setMwGlobals( array(
-                       'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
-                       'wgCookieExpiration' => $cookieExpiration,
-               ) );
-
-               $response = $this->getMock( 'WebResponse' );
-               $setcookieSpy = $this->any();
-               $response->expects( $setcookieSpy )
-                       ->method( 'setcookie' );
-
-               $request = new MockWebRequest( $response );
-               $user = new UserProxy( User::newFromSession( $request ) );
-               $user->setExtendedLoginCookie( 'name', 'value', true );
-
-               $setcookieInvocations = $setcookieSpy->getInvocations();
-               $setcookieInvocation = end( $setcookieInvocations );
-               $actualExpiry = $setcookieInvocation->parameters[2];
-
-               // TODO: ± 300 seconds compensates for
-               // slow-running tests. However, the dependency on the time
-               // function should be removed.  This requires some way
-               // to mock/isolate User->setExtendedLoginCookie's call to time()
-               $this->assertEquals( $expectedExpiry, $actualExpiry, '', 300 );
-       }
-}
-
-class UserProxy extends User {
-
-       /**
-        * @var User
-        */
-       protected $user;
-
-       public function __construct( User $user ) {
-               $this->user = $user;
-       }
-
-       public function setExtendedLoginCookie( $name, $value, $secure ) {
-               $this->user->setExtendedLoginCookie( $name, $value, $secure );
-       }
-}
index df4213a..736f6e8 100644 (file)
@@ -34,6 +34,31 @@ class DeferredUpdatesTest extends MediaWikiTestCase {
                $this->expectOutputString( implode( '', $updates ) );
 
                DeferredUpdates::doUpdates();
+
+               $x = null;
+               $y = null;
+               DeferredUpdates::addCallableUpdate(
+                       function () use ( &$x ) {
+                               $x = 'Sherity';
+                       },
+                       DeferredUpdates::PRESEND
+               );
+               DeferredUpdates::addCallableUpdate(
+                       function () use ( &$y ) {
+                               $y = 'Marychu';
+                       },
+                       DeferredUpdates::POSTSEND
+               );
+
+               $this->assertNull( $x, "Update not run yet" );
+               $this->assertNull( $y, "Update not run yet" );
+
+               DeferredUpdates::doUpdates( 'run', DeferredUpdates::PRESEND );
+               $this->assertEquals( "Sherity", $x, "PRESEND update ran" );
+               $this->assertNull( $y, "POSTSEND update not run yet" );
+
+               DeferredUpdates::doUpdates( 'run', DeferredUpdates::POSTSEND );
+               $this->assertEquals( "Marychu", $y, "POSTSEND update ran" );
        }
 
        public function testDoUpdatesCLI() {
index 25ee5ec..c8b4bda 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 /**
+ * @group LinksUpdate
  * @group Database
  * ^--- make sure temporary tables are used.
  */
@@ -147,6 +148,7 @@ class LinksUpdateTest extends MediaWikiTestCase {
                $title = Title::newFromText( 'Testing' );
                $wikiPage = new WikiPage( $title );
                $wikiPage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' );
+               $this->runAllRelatedJobs();
 
                $this->assertRecentChangeByCategorization(
                        $title,
@@ -155,7 +157,9 @@ class LinksUpdateTest extends MediaWikiTestCase {
                        array( array( 'Foo', '[[:Testing]] added to category' ) )
                );
 
-               $wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'added category' );
+               $wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'replaced category' );
+               $this->runAllRelatedJobs();
+
                $this->assertRecentChangeByCategorization(
                        $title,
                        $wikiPage->getParserOutput( new ParserOptions() ),
@@ -184,15 +188,27 @@ class LinksUpdateTest extends MediaWikiTestCase {
 
                $wikiPage = new WikiPage( Title::newFromText( 'Testing' ) );
                $wikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
+               $this->runAllRelatedJobs();
+
                $otherWikiPage = new WikiPage( Title::newFromText( 'Some_other_page' ) );
                $otherWikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
-               $templatePage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' );
+               $this->runAllRelatedJobs();
+
+               $this->assertRecentChangeByCategorization(
+                       $templateTitle,
+                       $templatePage->getParserOutput( new ParserOptions() ),
+                       Title::newFromText( 'Baz' ),
+                       array()
+               );
+
+               $templatePage->doEditContent( new WikitextContent( '[[Category:Baz]]' ), 'added category' );
+               $this->runAllRelatedJobs();
 
                $this->assertRecentChangeByCategorization(
                        $templateTitle,
                        $templatePage->getParserOutput( new ParserOptions() ),
-                       Title::newFromText( 'Foo' ),
-                       array( array( 'Foo', '[[:Template:TestingTemplate]] and 2 pages added to category' ) )
+                       Title::newFromText( 'Baz' ),
+                       array( array( 'Baz', '[[:Template:TestingTemplate]] and 2 pages added to category' ) )
                );
        }
 
@@ -330,13 +346,6 @@ class LinksUpdateTest extends MediaWikiTestCase {
        protected function assertRecentChangeByCategorization(
                Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows
        ) {
-               $update = new LinksUpdate( $pageTitle, $parserOutput );
-               $revision = Revision::newFromTitle( $pageTitle );
-               $update->setRevision( $revision );
-               $update->beginTransaction();
-               $update->doUpdate();
-               $update->commitTransaction();
-
                $this->assertSelect(
                        'recentchanges',
                        'rc_title, rc_comment',
@@ -348,4 +357,16 @@ class LinksUpdateTest extends MediaWikiTestCase {
                        $expectedRows
                );
        }
+
+       private function runAllRelatedJobs() {
+               $queueGroup = JobQueueGroup::singleton();
+               while ( $job = $queueGroup->pop( 'refreshLinksPrioritized' ) ) {
+                       $job->run();
+                       $queueGroup->ack( $job );
+               }
+               while ( $job = $queueGroup->pop( 'categoryMembershipChange' ) ) {
+                       $job->run();
+                       $queueGroup->ack( $job );
+               }
+       }
 }
diff --git a/tests/phpunit/includes/deferred/SquidUpdateTest.php b/tests/phpunit/includes/deferred/SquidUpdateTest.php
new file mode 100644 (file)
index 0000000..6ceb42c
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+class SquidUpdatesTest extends MediaWikiTestCase {
+       public function testPurgeMergeWeb() {
+               $this->setMwGlobals( 'wgCommandLineMode', false );
+
+               $urls1 = array();
+               $title = Title::newMainPage();
+               $urls1[] = $title->getCanonicalURL( '?x=1' );
+               $urls1[] = $title->getCanonicalURL( '?x=2' );
+               $urls1[] = $title->getCanonicalURL( '?x=3' );
+               $update1 = new SquidUpdate( $urls1 );
+               DeferredUpdates::addUpdate( $update1 );
+
+               $urls2 = array();
+               $urls2[] = $title->getCanonicalURL( '?x=2' );
+               $urls2[] = $title->getCanonicalURL( '?x=3' );
+               $urls2[] = $title->getCanonicalURL( '?x=4' );
+               $update2 = new SquidUpdate( $urls2 );
+               DeferredUpdates::addUpdate( $update2 );
+
+               $wrapper = TestingAccessWrapper::newFromObject( $update1 );
+               $this->assertEquals( array_merge( $urls1, $urls2 ), $wrapper->urls );
+       }
+}
diff --git a/tests/phpunit/includes/user/CentralIdLookupTest.php b/tests/phpunit/includes/user/CentralIdLookupTest.php
new file mode 100644 (file)
index 0000000..386e7ab
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @covers CentralIdLookup
+ * @group Database
+ */
+class CentralIdLookupTest extends MediaWikiTestCase {
+
+       public function testFactory() {
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+
+               $this->setMwGlobals( array(
+                       'wgCentralIdLookupProviders' => array(
+                               'local' => array( 'class' => 'LocalIdLookup' ),
+                               'local2' => array( 'class' => 'LocalIdLookup' ),
+                               'mock' => array( 'factory' => function () use ( $mock ) {
+                                       return $mock;
+                               } ),
+                               'bad' => array( 'class' => 'stdClass' ),
+                       ),
+                       'wgCentralIdLookupProvider' => 'mock',
+               ) );
+
+               $this->assertSame( $mock, CentralIdLookup::factory() );
+               $this->assertSame( $mock, CentralIdLookup::factory( 'mock' ) );
+               $this->assertSame( 'mock', $mock->getProviderId() );
+
+               $local = CentralIdLookup::factory( 'local' );
+               $this->assertNotSame( $mock, $local );
+               $this->assertInstanceOf( 'LocalIdLookup', $local );
+               $this->assertSame( $local, CentralIdLookup::factory( 'local' ) );
+               $this->assertSame( 'local', $local->getProviderId() );
+
+               $local2 = CentralIdLookup::factory( 'local2' );
+               $this->assertNotSame( $local, $local2 );
+               $this->assertInstanceOf( 'LocalIdLookup', $local2 );
+               $this->assertSame( 'local2', $local2->getProviderId() );
+
+               $this->assertNull( CentralIdLookup::factory( 'unconfigured' ) );
+               $this->assertNull( CentralIdLookup::factory( 'bad' ) );
+       }
+
+       public function testCheckAudience() {
+               $mock = TestingAccessWrapper::newFromObject(
+                       $this->getMockForAbstractClass( 'CentralIdLookup' )
+               );
+
+               $user = User::newFromName( 'UTSysop' );
+               $this->assertSame( $user, $mock->checkAudience( $user ) );
+
+               $user = $mock->checkAudience( CentralIdLookup::AUDIENCE_PUBLIC );
+               $this->assertInstanceOf( 'User', $user );
+               $this->assertSame( 0, $user->getId() );
+
+               $this->assertNull( $mock->checkAudience( CentralIdLookup::AUDIENCE_RAW ) );
+
+               try {
+                       $mock->checkAudience( 100 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid audience', $ex->getMessage() );
+               }
+       }
+
+       public function testNameFromCentralId() {
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->once() )->method( 'lookupCentralIds' )
+                       ->with(
+                               $this->equalTo( array( 15 => null ) ),
+                               $this->equalTo( CentralIdLookup::AUDIENCE_RAW ),
+                               $this->equalTo( CentralIdLookup::READ_LATEST )
+                       )
+                       ->will( $this->returnValue( array( 15 => 'FooBar' ) ) );
+
+               $this->assertSame(
+                       'FooBar',
+                       $mock->nameFromCentralId( 15, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST )
+               );
+       }
+
+       /**
+        * @dataProvider provideLocalUserFromCentralId
+        * @param string $name
+        * @param bool $succeeds
+        */
+       public function testLocalUserFromCentralId( $name, $succeeds ) {
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( true ) );
+               $mock->expects( $this->once() )->method( 'lookupCentralIds' )
+                       ->with(
+                               $this->equalTo( array( 42 => null ) ),
+                               $this->equalTo( CentralIdLookup::AUDIENCE_RAW ),
+                               $this->equalTo( CentralIdLookup::READ_LATEST )
+                       )
+                       ->will( $this->returnValue( array( 42 => $name ) ) );
+
+               $user = $mock->localUserFromCentralId(
+                       42, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
+               );
+               if ( $succeeds ) {
+                       $this->assertInstanceOf( 'User', $user );
+                       $this->assertSame( $name, $user->getName() );
+               } else {
+                       $this->assertNull( $user );
+               }
+
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( false ) );
+               $mock->expects( $this->once() )->method( 'lookupCentralIds' )
+                       ->with(
+                               $this->equalTo( array( 42 => null ) ),
+                               $this->equalTo( CentralIdLookup::AUDIENCE_RAW ),
+                               $this->equalTo( CentralIdLookup::READ_LATEST )
+                       )
+                       ->will( $this->returnValue( array( 42 => $name ) ) );
+               $this->assertNull(
+                       $mock->localUserFromCentralId( 42, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST )
+               );
+       }
+
+       public static function provideLocalUserFromCentralId() {
+               return array(
+                       array( 'UTSysop', true ),
+                       array( 'UTDoesNotExist', false ),
+                       array( null, false ),
+                       array( '', false ),
+                       array( '<X>', false ),
+               );
+       }
+
+       public function testCentralIdFromName() {
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->once() )->method( 'lookupUserNames' )
+                       ->with(
+                               $this->equalTo( array( 'FooBar' => 0 ) ),
+                               $this->equalTo( CentralIdLookup::AUDIENCE_RAW ),
+                               $this->equalTo( CentralIdLookup::READ_LATEST )
+                       )
+                       ->will( $this->returnValue( array( 'FooBar' => 23 ) ) );
+
+               $this->assertSame(
+                       23,
+                       $mock->centralIdFromName( 'FooBar', CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST )
+               );
+       }
+
+       public function testCentralIdFromLocalUser() {
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( true ) );
+               $mock->expects( $this->once() )->method( 'lookupUserNames' )
+                       ->with(
+                               $this->equalTo( array( 'FooBar' => 0 ) ),
+                               $this->equalTo( CentralIdLookup::AUDIENCE_RAW ),
+                               $this->equalTo( CentralIdLookup::READ_LATEST )
+                       )
+                       ->will( $this->returnValue( array( 'FooBar' => 23 ) ) );
+
+               $this->assertSame(
+                       23,
+                       $mock->centralIdFromLocalUser(
+                               User::newFromName( 'FooBar' ), CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
+                       )
+               );
+
+               $mock = $this->getMockForAbstractClass( 'CentralIdLookup' );
+               $mock->expects( $this->any() )->method( 'isAttached' )
+                       ->will( $this->returnValue( false ) );
+               $mock->expects( $this->never() )->method( 'lookupUserNames' );
+
+               $this->assertSame(
+                       0,
+                       $mock->centralIdFromLocalUser(
+                               User::newFromName( 'FooBar' ), CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
+                       )
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/user/LocalIdLookupTest.php b/tests/phpunit/includes/user/LocalIdLookupTest.php
new file mode 100644 (file)
index 0000000..2bea575
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @covers LocalIdLookup
+ * @group Database
+ */
+class LocalIdLookupTest extends MediaWikiTestCase {
+       private $localUsers = array();
+
+       protected function setUp() {
+               global $wgGroupPermissions;
+
+               parent::setUp();
+
+               $this->stashMwGlobals( array( 'wgGroupPermissions' ) );
+               $wgGroupPermissions['local-id-lookup-test']['hideuser'] = true;
+       }
+
+       public function addDBData() {
+               for ( $i = 1; $i <= 4; $i++ ) {
+                       $user = User::newFromName( "UTLocalIdLookup$i" );
+                       if ( $user->getId() == 0 ) {
+                               $user->addToDatabase();
+                       }
+                       $this->localUsers["UTLocalIdLookup$i"] = $user->getId();
+               }
+
+               User::newFromName( 'UTLocalIdLookup1' )->addGroup( 'local-id-lookup-test' );
+
+               $block = new Block( array(
+                       'address' => 'UTLocalIdLookup3',
+                       'by' => User::idFromName( 'UTSysop' ),
+                       'reason' => __METHOD__,
+                       'expiry' => '1 day',
+                       'hideName' => false,
+               ) );
+               $block->insert();
+
+               $block = new Block( array(
+                       'address' => 'UTLocalIdLookup4',
+                       'by' => User::idFromName( 'UTSysop' ),
+                       'reason' => __METHOD__,
+                       'expiry' => '1 day',
+                       'hideName' => true,
+               ) );
+               $block->insert();
+       }
+
+       public function testLookupCentralIds() {
+               $lookup = new LocalIdLookup();
+               $user1 = User::newFromName( 'UTLocalIdLookup1' );
+               $user2 = User::newFromName( 'UTLocalIdLookup2' );
+
+               $this->assertTrue( $user1->isAllowed( 'hideuser' ), 'sanity check' );
+               $this->assertFalse( $user2->isAllowed( 'hideuser' ), 'sanity check' );
+
+               $this->assertSame( array(), $lookup->lookupCentralIds( array() ) );
+
+               $expect = array_flip( $this->localUsers );
+               $expect[123] = 'X';
+               ksort( $expect );
+
+               $expect2 = $expect;
+               $expect2[$this->localUsers['UTLocalIdLookup4']] = '';
+
+               $arg = array_fill_keys( array_keys( $expect ), 'X' );
+
+               $this->assertSame( $expect2, $lookup->lookupCentralIds( $arg ) );
+               $this->assertSame( $expect, $lookup->lookupCentralIds( $arg, CentralIdLookup::AUDIENCE_RAW ) );
+               $this->assertSame( $expect, $lookup->lookupCentralIds( $arg, $user1 ) );
+               $this->assertSame( $expect2, $lookup->lookupCentralIds( $arg, $user2 ) );
+       }
+
+       public function testLookupUserNames() {
+               $lookup = new LocalIdLookup();
+               $user1 = User::newFromName( 'UTLocalIdLookup1' );
+               $user2 = User::newFromName( 'UTLocalIdLookup2' );
+
+               $this->assertTrue( $user1->isAllowed( 'hideuser' ), 'sanity check' );
+               $this->assertFalse( $user2->isAllowed( 'hideuser' ), 'sanity check' );
+
+               $this->assertSame( array(), $lookup->lookupUserNames( array() ) );
+
+               $expect = $this->localUsers;
+               $expect['UTDoesNotExist'] = 'X';
+               ksort( $expect );
+
+               $expect2 = $expect;
+               $expect2['UTLocalIdLookup4'] = 'X';
+
+               $arg = array_fill_keys( array_keys( $expect ), 'X' );
+
+               $this->assertSame( $expect2, $lookup->lookupUserNames( $arg ) );
+               $this->assertSame( $expect, $lookup->lookupUserNames( $arg, CentralIdLookup::AUDIENCE_RAW ) );
+               $this->assertSame( $expect, $lookup->lookupUserNames( $arg, $user1 ) );
+               $this->assertSame( $expect2, $lookup->lookupUserNames( $arg, $user2 ) );
+       }
+
+       public function testIsAttached() {
+               $lookup = new LocalIdLookup();
+               $user1 = User::newFromName( 'UTLocalIdLookup1' );
+               $user2 = User::newFromName( 'DoesNotExist' );
+
+               $this->assertTrue( $lookup->isAttached( $user1 ) );
+               $this->assertFalse( $lookup->isAttached( $user2 ) );
+
+               $wiki = wfWikiId();
+               $this->assertTrue( $lookup->isAttached( $user1, $wiki ) );
+               $this->assertFalse( $lookup->isAttached( $user2, $wiki ) );
+
+               $wiki = 'not-' . wfWikiId();
+               $this->assertFalse( $lookup->isAttached( $user1, $wiki ) );
+               $this->assertFalse( $lookup->isAttached( $user2, $wiki ) );
+       }
+
+       /**
+        * @dataProvider provideIsAttachedShared
+        * @param bool $sharedDB $wgSharedDB is set
+        * @param bool $sharedTable $wgSharedTables contains 'user'
+        * @param bool $localDBSet $wgLocalDatabases contains the shared DB
+        */
+       public function testIsAttachedShared( $sharedDB, $sharedTable, $localDBSet ) {
+               global $wgDBName;
+               $this->setMwGlobals( array(
+                       'wgSharedDB' => $sharedDB ? $wgDBName : null,
+                       'wgSharedTables' => $sharedTable ? array( 'user' ) : array(),
+                       'wgLocalDatabases' => $localDBSet ? array( 'shared' ) : array(),
+               ) );
+
+               $lookup = new LocalIdLookup();
+               $this->assertSame(
+                       $sharedDB && $sharedTable && $localDBSet,
+                       $lookup->isAttached( User::newFromName( 'UTLocalIdLookup1' ), 'shared' )
+               );
+       }
+
+       public static function provideIsAttachedShared() {
+               $ret = array();
+               for ( $i = 0; $i < 7; $i++ ) {
+                       $ret[] = array(
+                               (bool)( $i & 1 ),
+                               (bool)( $i & 2 ),
+                               (bool)( $i & 4 ),
+                       );
+               }
+               return $ret;
+       }
+
+}
diff --git a/tests/phpunit/includes/user/UserArrayFromResultTest.php b/tests/phpunit/includes/user/UserArrayFromResultTest.php
new file mode 100644 (file)
index 0000000..469ad29
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @author Adam Shorland
+ * @covers UserArrayFromResult
+ */
+class UserArrayFromResultTest extends MediaWikiTestCase {
+
+       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+               $resultWrapper = $this->getMockBuilder( 'ResultWrapper' )
+                       ->disableOriginalConstructor();
+
+               $resultWrapper = $resultWrapper->getMock();
+               $resultWrapper->expects( $this->atLeastOnce() )
+                       ->method( 'current' )
+                       ->will( $this->returnValue( $row ) );
+               $resultWrapper->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( $numRows ) );
+
+               return $resultWrapper;
+       }
+
+       private function getRowWithUsername( $username = 'fooUser' ) {
+               $row = new stdClass();
+               $row->user_name = $username;
+               return $row;
+       }
+
+       private function getUserArrayFromResult( $resultWrapper ) {
+               return new UserArrayFromResult( $resultWrapper );
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithFalseRow() {
+               $row = false;
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = $this->getUserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertEquals( $row, $object->current );
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithRow() {
+               $username = 'addshore';
+               $row = $this->getRowWithUsername( $username );
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = $this->getUserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertInstanceOf( 'User', $object->current );
+               $this->assertEquals( $username, $object->current->mName );
+       }
+
+       public static function provideNumberOfRows() {
+               return array(
+                       array( 0 ),
+                       array( 1 ),
+                       array( 122 ),
+               );
+       }
+
+       /**
+        * @dataProvider provideNumberOfRows
+        * @covers UserArrayFromResult::count
+        */
+       public function testCountWithVaryingValues( $numRows ) {
+               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper(
+                       $this->getRowWithUsername(),
+                       $numRows
+               ) );
+               $this->assertEquals( $numRows, $object->count() );
+       }
+
+       /**
+        * @covers UserArrayFromResult::current
+        */
+       public function testCurrentAfterConstruction() {
+               $username = 'addshore';
+               $userRow = $this->getRowWithUsername( $username );
+               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
+               $this->assertInstanceOf( 'User', $object->current() );
+               $this->assertEquals( $username, $object->current()->mName );
+       }
+
+       public function provideTestValid() {
+               return array(
+                       array( $this->getRowWithUsername(), true ),
+                       array( false, false ),
+               );
+       }
+
+       /**
+        * @dataProvider provideTestValid
+        * @covers UserArrayFromResult::valid
+        */
+       public function testValid( $input, $expected ) {
+               $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $this->assertEquals( $expected, $object->valid() );
+       }
+
+       // @todo unit test for key()
+       // @todo unit test for next()
+       // @todo unit test for rewind()
+}
diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php
new file mode 100644 (file)
index 0000000..45c4b8c
--- /dev/null
@@ -0,0 +1,534 @@
+<?php
+
+define( 'NS_UNITTEST', 5600 );
+define( 'NS_UNITTEST_TALK', 5601 );
+
+/**
+ * @group Database
+ */
+class UserTest extends MediaWikiTestCase {
+       /**
+        * @var User
+        */
+       protected $user;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgGroupPermissions' => array(),
+                       'wgRevokePermissions' => array(),
+               ) );
+
+               $this->setUpPermissionGlobals();
+
+               $this->user = new User;
+               $this->user->addGroup( 'unittesters' );
+       }
+
+       private function setUpPermissionGlobals() {
+               global $wgGroupPermissions, $wgRevokePermissions;
+
+               # Data for regular $wgGroupPermissions test
+               $wgGroupPermissions['unittesters'] = array(
+                       'test' => true,
+                       'runtest' => true,
+                       'writetest' => false,
+                       'nukeworld' => false,
+               );
+               $wgGroupPermissions['testwriters'] = array(
+                       'test' => true,
+                       'writetest' => true,
+                       'modifytest' => true,
+               );
+
+               # Data for regular $wgRevokePermissions test
+               $wgRevokePermissions['formertesters'] = array(
+                       'runtest' => true,
+               );
+
+               # For the options test
+               $wgGroupPermissions['*'] = array(
+                       'editmyoptions' => true,
+               );
+       }
+
+       /**
+        * @covers User::getGroupPermissions
+        */
+       public function testGroupPermissions() {
+               $rights = User::getGroupPermissions( array( 'unittesters' ) );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+
+               $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertContains( 'writetest', $rights );
+               $this->assertContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @covers User::getGroupPermissions
+        */
+       public function testRevokePermissions() {
+               $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @covers User::getRights
+        */
+       public function testUserPermissions() {
+               $rights = $this->user->getRights();
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'modifytest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
+       /**
+        * @dataProvider provideGetGroupsWithPermission
+        * @covers User::getGroupsWithPermission
+        */
+       public function testGetGroupsWithPermission( $expected, $right ) {
+               $result = User::getGroupsWithPermission( $right );
+               sort( $result );
+               sort( $expected );
+
+               $this->assertEquals( $expected, $result, "Groups with permission $right" );
+       }
+
+       public static function provideGetGroupsWithPermission() {
+               return array(
+                       array(
+                               array( 'unittesters', 'testwriters' ),
+                               'test'
+                       ),
+                       array(
+                               array( 'unittesters' ),
+                               'runtest'
+                       ),
+                       array(
+                               array( 'testwriters' ),
+                               'writetest'
+                       ),
+                       array(
+                               array( 'testwriters' ),
+                               'modifytest'
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideIPs
+        * @covers User::isIP
+        */
+       public function testIsIP( $value, $result, $message ) {
+               $this->assertEquals( $this->user->isIP( $value ), $result, $message );
+       }
+
+       public static function provideIPs() {
+               return array(
+                       array( '', false, 'Empty string' ),
+                       array( ' ', false, 'Blank space' ),
+                       array( '10.0.0.0', true, 'IPv4 private 10/8' ),
+                       array( '10.255.255.255', true, 'IPv4 private 10/8' ),
+                       array( '192.168.1.1', true, 'IPv4 private 192.168/16' ),
+                       array( '203.0.113.0', true, 'IPv4 example' ),
+                       array( '2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff', true, 'IPv6 example' ),
+                       // Not valid IPs but classified as such by MediaWiki for negated asserting
+                       // of whether this might be the identifier of a logged-out user or whether
+                       // to allow usernames like it.
+                       array( '300.300.300.300', true, 'Looks too much like an IPv4 address' ),
+                       array( '203.0.113.xxx', true, 'Assigned by UseMod to cloaked logged-out users' ),
+               );
+       }
+
+       /**
+        * @dataProvider provideUserNames
+        * @covers User::isValidUserName
+        */
+       public function testIsValidUserName( $username, $result, $message ) {
+               $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message );
+       }
+
+       public static function provideUserNames() {
+               return array(
+                       array( '', false, 'Empty string' ),
+                       array( ' ', false, 'Blank space' ),
+                       array( 'abcd', false, 'Starts with small letter' ),
+                       array( 'Ab/cd', false, 'Contains slash' ),
+                       array( 'Ab cd', true, 'Whitespace' ),
+                       array( '192.168.1.1', false, 'IP' ),
+                       array( 'User:Abcd', false, 'Reserved Namespace' ),
+                       array( '12abcd232', true, 'Starts with Numbers' ),
+                       array( '?abcd', true, 'Start with ? mark' ),
+                       array( '#abcd', false, 'Start with #' ),
+                       array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ),
+                       array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ),
+                       array( 'Ab cd', false, ' Ideographic space' ),
+                       array( '300.300.300.300', false, 'Looks too much like an IPv4 address' ),
+                       array( '302.113.311.900', false, 'Looks too much like an IPv4 address' ),
+                       array( '203.0.113.xxx', false, 'Reserved for usage by UseMod for cloaked logged-out users' ),
+               );
+       }
+
+       /**
+        * Test, if for all rights a right- message exist,
+        * which is used on Special:ListGroupRights as help text
+        * Extensions and core
+        */
+       public function testAllRightsWithMessage() {
+               // Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
+               $allRights = User::getAllRights();
+               $allMessageKeys = Language::getMessageKeysFor( 'en' );
+
+               $rightsWithMessage = array();
+               foreach ( $allMessageKeys as $message ) {
+                       // === 0: must be at beginning of string (position 0)
+                       if ( strpos( $message, 'right-' ) === 0 ) {
+                               $rightsWithMessage[] = substr( $message, strlen( 'right-' ) );
+                       }
+               }
+
+               sort( $allRights );
+               sort( $rightsWithMessage );
+
+               $this->assertEquals(
+                       $allRights,
+                       $rightsWithMessage,
+                       'Each user rights (core/extensions) has a corresponding right- message.'
+               );
+       }
+
+       /**
+        * Test User::editCount
+        * @group medium
+        * @covers User::getEditCount
+        */
+       public function testEditCount() {
+               $user = User::newFromName( 'UnitTestUser' );
+
+               if ( !$user->getId() ) {
+                       $user->addToDatabase();
+               }
+
+               // let the user have a few (3) edits
+               $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $page->doEdit( (string)$i, 'test', 0, false, $user );
+               }
+
+               $user->clearInstanceCache();
+               $this->assertEquals(
+                       3,
+                       $user->getEditCount(),
+                       'After three edits, the user edit count should be 3'
+               );
+
+               // increase the edit count and clear the cache
+               $user->incEditCount();
+
+               $user->clearInstanceCache();
+               $this->assertEquals(
+                       4,
+                       $user->getEditCount(),
+                       'After increasing the edit count manually, the user edit count should be 4'
+               );
+       }
+
+       /**
+        * Test changing user options.
+        * @covers User::setOption
+        * @covers User::getOption
+        */
+       public function testOptions() {
+               $user = User::newFromName( 'UnitTestUser' );
+
+               if ( !$user->getId() ) {
+                       $user->addToDatabase();
+               }
+
+               $user->setOption( 'userjs-someoption', 'test' );
+               $user->setOption( 'cols', 200 );
+               $user->saveSettings();
+
+               $user = User::newFromName( 'UnitTestUser' );
+               $this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) );
+               $this->assertEquals( 200, $user->getOption( 'cols' ) );
+       }
+
+       /**
+        * Bug 37963
+        * Make sure defaults are loaded when setOption is called.
+        * @covers User::loadOptions
+        */
+       public function testAnonOptions() {
+               global $wgDefaultUserOptions;
+               $this->user->setOption( 'userjs-someoption', 'test' );
+               $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) );
+               $this->assertEquals( 'test', $this->user->getOption( 'userjs-someoption' ) );
+       }
+
+       /**
+        * Test password validity checks. There are 3 checks in core,
+        *      - ensure the password meets the minimal length
+        *      - ensure the password is not the same as the username
+        *      - ensure the username/password combo isn't forbidden
+        * @covers User::checkPasswordValidity()
+        * @covers User::getPasswordValidity()
+        * @covers User::isValidPassword()
+        */
+       public function testCheckPasswordValidity() {
+               $this->setMwGlobals( array(
+                       'wgPasswordPolicy' => array(
+                               'policies' => array(
+                                       'sysop' => array(
+                                               'MinimalPasswordLength' => 8,
+                                               'MinimumPasswordLengthToLogin' => 1,
+                                               'PasswordCannotMatchUsername' => 1,
+                                       ),
+                                       'default' => array(
+                                               'MinimalPasswordLength' => 6,
+                                               'PasswordCannotMatchUsername' => true,
+                                               'PasswordCannotMatchBlacklist' => true,
+                                               'MaximalPasswordLength' => 30,
+                                       ),
+                               ),
+                               'checks' => array(
+                                       'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength',
+                                       'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
+                                       'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
+                                       'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
+                                       'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
+                               ),
+                       ),
+               ) );
+
+               $user = User::newFromName( 'Useruser' );
+               // Sanity
+               $this->assertTrue( $user->isValidPassword( 'Password1234' ) );
+
+               // Minimum length
+               $this->assertFalse( $user->isValidPassword( 'a' ) );
+               $this->assertFalse( $user->checkPasswordValidity( 'a' )->isGood() );
+               $this->assertTrue( $user->checkPasswordValidity( 'a' )->isOK() );
+               $this->assertEquals( 'passwordtooshort', $user->getPasswordValidity( 'a' ) );
+
+               // Maximum length
+               $longPass = str_repeat( 'a', 31 );
+               $this->assertFalse( $user->isValidPassword( $longPass ) );
+               $this->assertFalse( $user->checkPasswordValidity( $longPass )->isGood() );
+               $this->assertFalse( $user->checkPasswordValidity( $longPass )->isOK() );
+               $this->assertEquals( 'passwordtoolong', $user->getPasswordValidity( $longPass ) );
+
+               // Matches username
+               $this->assertFalse( $user->checkPasswordValidity( 'Useruser' )->isGood() );
+               $this->assertTrue( $user->checkPasswordValidity( 'Useruser' )->isOK() );
+               $this->assertEquals( 'password-name-match', $user->getPasswordValidity( 'Useruser' ) );
+
+               // On the forbidden list
+               $this->assertFalse( $user->checkPasswordValidity( 'Passpass' )->isGood() );
+               $this->assertEquals( 'password-login-forbidden', $user->getPasswordValidity( 'Passpass' ) );
+       }
+
+       /**
+        * @covers User::getCanonicalName()
+        * @dataProvider provideGetCanonicalName
+        */
+       public function testGetCanonicalName( $name, $expectedArray, $msg ) {
+               foreach ( $expectedArray as $validate => $expected ) {
+                       $this->assertEquals(
+                               $expected,
+                               User::getCanonicalName( $name, $validate === 'false' ? false : $validate ),
+                               $msg . ' (' . $validate . ')'
+                       );
+               }
+       }
+
+       public static function provideGetCanonicalName() {
+               return array(
+                       array( ' Trailing space ', array( 'creatable' => 'Trailing space' ), 'Trailing spaces' ),
+                       // @todo FIXME: Maybe the creatable name should be 'Talk:Username' or false to reject?
+                       array( 'Talk:Username', array( 'creatable' => 'Username', 'usable' => 'Username',
+                               'valid' => 'Username', 'false' => 'Talk:Username' ), 'Namespace prefix' ),
+                       array( ' name with # hash', array( 'creatable' => false, 'usable' => false ), 'With hash' ),
+                       array( 'Multi  spaces', array( 'creatable' => 'Multi spaces',
+                               'usable' => 'Multi spaces' ), 'Multi spaces' ),
+                       array( 'lowercase', array( 'creatable' => 'Lowercase' ), 'Lowercase' ),
+                       array( 'in[]valid', array( 'creatable' => false, 'usable' => false, 'valid' => false,
+                               'false' => 'In[]valid' ), 'Invalid' ),
+                       array( 'with / slash', array( 'creatable' => false, 'usable' => false, 'valid' => false,
+                               'false' => 'With / slash' ), 'With slash' ),
+               );
+       }
+
+       /**
+        * @covers User::equals
+        */
+       public function testEquals() {
+               $first = User::newFromName( 'EqualUser' );
+               $second = User::newFromName( 'EqualUser' );
+
+               $this->assertTrue( $first->equals( $first ) );
+               $this->assertTrue( $first->equals( $second ) );
+               $this->assertTrue( $second->equals( $first ) );
+
+               $third = User::newFromName( '0' );
+               $fourth = User::newFromName( '000' );
+
+               $this->assertFalse( $third->equals( $fourth ) );
+               $this->assertFalse( $fourth->equals( $third ) );
+
+               // Test users loaded from db with id
+               $user = User::newFromName( 'EqualUnitTestUser' );
+               if ( !$user->getId() ) {
+                       $user->addToDatabase();
+               }
+
+               $id = $user->getId();
+
+               $fifth = User::newFromId( $id );
+               $sixth = User::newFromName( 'EqualUnitTestUser' );
+               $this->assertTrue( $fifth->equals( $sixth ) );
+       }
+
+       /**
+        * @covers User::getId
+        */
+       public function testGetId() {
+               $user = User::newFromName( 'UTSysop' );
+               $this->assertTrue( $user->getId() > 0 );
+
+       }
+
+       /**
+        * @covers User::isLoggedIn
+        * @covers User::isAnon
+        */
+       public function testLoggedIn() {
+               $user = User::newFromName( 'UTSysop' );
+               $this->assertTrue( $user->isLoggedIn() );
+               $this->assertFalse( $user->isAnon() );
+
+               // Non-existent users are perceived as anonymous
+               $user = User::newFromName( 'UTNonexistent' );
+               $this->assertFalse( $user->isLoggedIn() );
+               $this->assertTrue( $user->isAnon() );
+
+               $user = new User;
+               $this->assertFalse( $user->isLoggedIn() );
+               $this->assertTrue( $user->isAnon() );
+       }
+
+       /**
+        * @covers User::checkAndSetTouched
+        */
+       public function testCheckAndSetTouched() {
+               $user = TestingAccessWrapper::newFromObject( User::newFromName( 'UTSysop' ) );
+               $this->assertTrue( $user->isLoggedIn() );
+
+               $touched = $user->getDBTouched();
+               $this->assertTrue(
+                       $user->checkAndSetTouched(), "checkAndSetTouched() succeded" );
+               $this->assertGreaterThan(
+                       $touched, $user->getDBTouched(), "user_touched increased with casOnTouched()" );
+
+               $touched = $user->getDBTouched();
+               $this->assertTrue(
+                       $user->checkAndSetTouched(), "checkAndSetTouched() succeded #2" );
+               $this->assertGreaterThan(
+                       $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
+       }
+
+       public static function setExtendedLoginCookieDataProvider() {
+               $data = array();
+               $now = time();
+
+               $secondsInDay = 86400;
+
+               // Arbitrary durations, in units of days, to ensure it chooses the
+               // right one.  There is a 5-minute grace period (see testSetExtendedLoginCookie)
+               // to work around slow tests, since we're not currently mocking time() for PHP.
+
+               $durationOne = $secondsInDay * 5;
+               $durationTwo = $secondsInDay * 29;
+               $durationThree = $secondsInDay * 17;
+
+               // If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
+               // set cookie is time() + $wgCookieExpiration
+               $data[] = array(
+                       null,
+                       $durationOne,
+                       $now + $durationOne,
+               );
+
+               // If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
+               // set cookie is $now + $wgExtendedLoginCookieExpiration
+               $data[] = array(
+                       $durationTwo,
+                       $durationThree,
+                       $now + $durationTwo,
+               );
+
+               return $data;
+       }
+
+       /**
+        * @dataProvider setExtendedLoginCookieDataProvider
+        * @covers User::getRequest
+        * @covers User::setCookie
+        * @backupGlobals enabled
+        */
+       public function testSetExtendedLoginCookie(
+               $extendedLoginCookieExpiration,
+               $cookieExpiration,
+               $expectedExpiry
+       ) {
+               $this->setMwGlobals( array(
+                       'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
+                       'wgCookieExpiration' => $cookieExpiration,
+               ) );
+
+               $response = $this->getMock( 'WebResponse' );
+               $setcookieSpy = $this->any();
+               $response->expects( $setcookieSpy )
+                       ->method( 'setcookie' );
+
+               $request = new MockWebRequest( $response );
+               $user = new UserProxy( User::newFromSession( $request ) );
+               $user->setExtendedLoginCookie( 'name', 'value', true );
+
+               $setcookieInvocations = $setcookieSpy->getInvocations();
+               $setcookieInvocation = end( $setcookieInvocations );
+               $actualExpiry = $setcookieInvocation->parameters[2];
+
+               // TODO: ± 600 seconds compensates for
+               // slow-running tests. However, the dependency on the time
+               // function should be removed.  This requires some way
+               // to mock/isolate User->setExtendedLoginCookie's call to time()
+               $this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 );
+       }
+}
+
+class UserProxy extends User {
+
+       /**
+        * @var User
+        */
+       protected $user;
+
+       public function __construct( User $user ) {
+               $this->user = $user;
+       }
+
+       public function setExtendedLoginCookie( $name, $value, $secure ) {
+               $this->user->setExtendedLoginCookie( $name, $value, $secure );
+       }
+}