Merge "API: Add ApiQueryAllRevisions"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 7 Oct 2015 16:38:35 +0000 (16:38 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 7 Oct 2015 16:38:35 +0000 (16:38 +0000)
91 files changed:
RELEASE-NOTES-1.27
autoload.php
docs/hooks.txt
includes/CategoryFinder.php
includes/DefaultSettings.php
includes/FileDeleteForm.php
includes/LinkFilter.php
includes/MediaWiki.php
includes/OutputPage.php
includes/RevisionList.php
includes/SiteStats.php
includes/Title.php
includes/User.php
includes/UserRightsProxy.php
includes/WebRequest.php
includes/WebRequestUpload.php [new file with mode: 0644]
includes/WikiMap.php
includes/actions/InfoAction.php
includes/api/ApiQueryFileRepoInfo.php
includes/api/i18n/gl.json
includes/api/i18n/ksh.json
includes/api/i18n/nl.json
includes/api/i18n/ps.json
includes/changetags/ChangeTags.php
includes/db/Database.php
includes/db/DatabaseError.php
includes/db/loadbalancer/LBFactory.php
includes/db/loadbalancer/LoadBalancer.php
includes/externalstore/ExternalStoreDB.php
includes/filebackend/FileBackendGroup.php
includes/filebackend/filejournal/DBFileJournal.php
includes/filebackend/lockmanager/DBLockManager.php
includes/filerepo/ForeignDBRepo.php
includes/filerepo/ForeignDBViaLBRepo.php
includes/filerepo/LocalRepo.php
includes/filerepo/file/File.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/interwiki/Interwiki.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/AssembleUploadChunksJob.php
includes/jobqueue/jobs/PublishStashedFileJob.php
includes/libs/MemoizedCallable.php [new file with mode: 0644]
includes/libs/MultiHttpClient.php
includes/libs/ObjectFactory.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/objectcache/XCacheBagOStuff.php
includes/mail/UserMailer.php
includes/objectcache/MemcachedBagOStuff.php
includes/objectcache/MemcachedPeclBagOStuff.php
includes/objectcache/MultiWriteBagOStuff.php
includes/objectcache/RedisBagOStuff.php
includes/objectcache/SqlBagOStuff.php
includes/page/WikiFilePage.php
includes/page/WikiPage.php
includes/pager/IndexPager.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/site/DBSiteStore.php
includes/site/SiteSQLStore.php
includes/specials/SpecialSearch.php
includes/utils/BatchRowIterator.php
includes/utils/BatchRowWriter.php
includes/utils/FileContentsHasher.php
languages/i18n/be-tarask.json
languages/i18n/cu.json
languages/i18n/diq.json
languages/i18n/en.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/jv.json
languages/i18n/ksh.json
languages/i18n/nah.json
languages/i18n/nl.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/ru.json
phpcs.xml
resources/Resources.php
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
resources/src/mediawiki/mediawiki.ForeignUpload.js
tests/phpunit/includes/WikiMapTest.php
tests/phpunit/includes/changes/OldChangesListTest.php
tests/phpunit/includes/libs/MemoizedCallableTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.ForeignUpload.test.js

index 5d26ee9..9aaa32e 100644 (file)
@@ -16,11 +16,23 @@ production.
   1000 for the latter) are now hard-coded.
 * $wgDebugDumpSqlLength was removed (deprecated in 1.24).
 * $wgDebugDBTransactions was removed (deprecated in 1.20).
+* $wgRemoteUploadTarget (added in 1.26) removed, replaced by $wgForeignUploadTargets
 
 === New features in 1.27 ===
 * $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
   basic configuration settings needed for multi-datacenter setups.
   $wgDataCenterUpdateStickTTL was also added.
+* Added a new hook, 'UserMailerTransformContent', to transform the contents
+  of an email. This is similar to the EmailUser hook but applies to all mail
+  sent via UserMailer.
+* Added a new hook, 'UserMailerTransformMessage', to transform the contents
+  of an emai after MIME encoding.
+* Added a new hook, 'UserMailerSplitTo', to control which users have to be
+  emailed separately (ie. there is a single address in the To: field) so
+  user-specific changes to the email can be applied safely.
+* $wgCdnMaxageLagged was added, which limits the CDN cache TTL
+  when any load balancer uses a DB that is lagged beyond the 'max lag'
+  setting in the relevant section of $wgLBFactoryConf.
 
 ==== External libraries ====
 
index dee87c0..a2f432f 100644 (file)
@@ -288,6 +288,7 @@ $wgAutoloadLocalClasses = array(
        'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
        'DBMasterPos' => __DIR__ . '/includes/db/DatabaseUtility.php',
        'DBQueryError' => __DIR__ . '/includes/db/DatabaseError.php',
+       'DBReadOnlyError' => __DIR__ . '/includes/db/DatabaseError.php',
        'DBSiteStore' => __DIR__ . '/includes/site/DBSiteStore.php',
        'DBUnexpectedError' => __DIR__ . '/includes/db/DatabaseError.php',
        'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
@@ -782,6 +783,7 @@ $wgAutoloadLocalClasses = array(
        'MemcachedBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedBagOStuff.php',
        'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPhpBagOStuff.php',
+       'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
        'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
        'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
@@ -1375,7 +1377,7 @@ $wgAutoloadLocalClasses = array(
        'WebInstallerWelcome' => __DIR__ . '/includes/installer/WebInstallerPage.php',
        'WebPHandler' => __DIR__ . '/includes/media/WebP.php',
        'WebRequest' => __DIR__ . '/includes/WebRequest.php',
-       'WebRequestUpload' => __DIR__ . '/includes/WebRequest.php',
+       'WebRequestUpload' => __DIR__ . '/includes/WebRequestUpload.php',
        'WebResponse' => __DIR__ . '/includes/WebResponse.php',
        'WikiCategoryPage' => __DIR__ . '/includes/page/WikiCategoryPage.php',
        'WikiDiff3' => __DIR__ . '/includes/diff/WikiDiff3.php',
index 2d268b8..717dd39 100644 (file)
@@ -2907,6 +2907,7 @@ $term: Search term specified by the user
 on the search results page.  Useful for including a feedback link.
 $specialSearch: SpecialSearch object ($this)
 $output: $wgOut
+$term: Search term specified by the user
 
 'SpecialSearchSetupEngine': Allows passing custom data to search engine.
 $search: SpecialSearch special page object
@@ -3292,6 +3293,26 @@ when UserMailer sends an email, with a bounce handling extension.
 $to: Array of MailAddress objects for the recipients
 &$returnPath: The return address string
 
+'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance
+to split up an email with multiple the To: field into separate emails.
+$to: array of MailAddress objects; unset the ones which should be mailed separately
+
+'UserMailerTransformContent': Called in UserMailer::send() to change email contents.
+Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys
+&$error: should be set to an error message string
+
+'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through
+the MIME transform. Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$subject: email subject (not MIME encoded)
+&$headers: email headers (except To: and Subject:) as an array of header name => value pairs
+&$body: email body (in MIME format) as a string
+&$error: should be set to an error message string
+
 'UserRemoveGroup': Called when removing a group; return false to override stock
 group removal.
 $user: the user object that is to have a group removed
index 77c43bf..d779141 100644 (file)
@@ -64,7 +64,7 @@ class CategoryFinder {
        /** @var string "AND" or "OR" */
        protected $mode;
 
-       /** @var DatabaseBase Read-DB slave */
+       /** @var IDatabase Read-DB slave */
        protected $dbr;
 
        /**
index deb85f5..95baa56 100644 (file)
@@ -526,12 +526,11 @@ $wgForeignFileRepos = array();
 $wgUseInstantCommons = false;
 
 /**
- * Name of the remote repository to which users will be allowed to upload
- * files in their editors. Used to find a set of message names to describe
- * the legal requirements for uploading to that wiki, and suggestions for
- * when those requirements are not met.
+ * Array of foreign file repos (set in $wgForeignFileRepos above) that
+ * are allowable upload targets. These wikis must have some method of
+ * authentication (i.e. CentralAuth), and be CORS-enabled for this wiki.
  */
-$wgRemoteUploadTarget = 'default';
+$wgForeignUploadTargets = array();
 
 /**
  * File backend structure configuration.
@@ -2567,14 +2566,21 @@ $wgVaryOnXFP = false;
 $wgInternalServer = false;
 
 /**
- * Cache timeout for the squid, will be sent as s-maxage (without ESI) or
- * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in
- * the Squid config.
+ * Cache TTL for the CDN sent as s-maxage (without ESI) or
+ * Surrogate-Control (with ESI). Without ESI, you should strip
+ * out s-maxage in the Squid config.
  *
-* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
+ * 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
  */
 $wgSquidMaxage = 18000;
 
+/**
+ * Cache timeout for the CDN when DB slave lag is high
+ * @see $wgSquidMaxage
+ * @since 1.27
+ */
+$wgCdnMaxageLagged = 30;
+
 /**
  * Default maximum age for raw CSS/JS accesses
  *
index ae186bc..5e7f5b2 100644 (file)
@@ -180,6 +180,8 @@ class FileDeleteForm {
                                $logEntry->setComment( $logComment );
                                $logid = $logEntry->insert();
                                $logEntry->publish( $logid );
+
+                               $status->value = $logid;
                        }
                } else {
                        $status = Status::newFatal( 'cannotdelete',
@@ -195,7 +197,10 @@ class FileDeleteForm {
                                // or revision is missing, so check for isOK() rather than isGood()
                                if ( $deleteStatus->isOK() ) {
                                        $status = $file->delete( $reason, $suppress, $user );
-                                       if ( !$status->isOK() ) {
+                                       if ( $status->isOK() ) {
+                                               $dbw->commit( __METHOD__ );
+                                               $status->value = $deleteStatus->value; // log id
+                                       } else {
                                                $dbw->rollback( __METHOD__ );
                                        }
                                }
index 7215cec..802bfbe 100644 (file)
@@ -71,7 +71,7 @@ class LinkFilter {
        }
 
        /**
-        * Make an array to be used for calls to DatabaseBase::buildLike(), which
+        * Make an array to be used for calls to Database::buildLike(), which
         * will match the specified string. There are several kinds of filter entry:
         *     *.domain.com    -  Produces http://com.domain.%, matches domain.com
         *                        and www.domain.com
@@ -89,7 +89,7 @@ class LinkFilter {
         *
         * @param string $filterEntry Domainparts
         * @param string $protocol Protocol (default http://)
-        * @return array Array to be passed to DatabaseBase::buildLike() or false on error
+        * @return array Array to be passed to Database::buildLike() or false on error
         */
        public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
                $db = wfGetDB( DB_SLAVE );
index e29319b..418ed8b 100644 (file)
@@ -511,6 +511,13 @@ class MediaWiki {
                        $expires = time() + $this->config->get( 'DataCenterUpdateStickTTL' );
                        $request->response()->setCookie( 'UseDC', 'master', $expires );
                }
+
+               // Avoid letting a few seconds of slave lag cause a month of stale data
+               if ( $factory->laggedSlaveUsed() ) {
+                       $maxAge = $this->config->get( 'CdnMaxageLagged' );
+                       $this->context->getOutput()->lowerCdnMaxage( $maxAge );
+                       wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
+               }
        }
 
        /**
index f680d45..03ae8c9 100644 (file)
@@ -236,6 +236,8 @@ class OutputPage extends ContextSource {
 
        /** @var int Cache stuff. Looks like mEnableClientCache */
        protected $mSquidMaxage = 0;
+       /** @var in Upper limit on mSquidMaxage */
+       protected $mCdnMaxageLimit = INF;
 
        /**
         * @var bool Controls if anti-clickjacking / frame-breaking headers will
@@ -1945,7 +1947,18 @@ class OutputPage extends ContextSource {
         * @param int $maxage Maximum cache time on the Squid, in seconds.
         */
        public function setSquidMaxage( $maxage ) {
-               $this->mSquidMaxage = $maxage;
+               $this->mSquidMaxage = min( $maxage, $this->mCdnMaxageLimit );
+       }
+
+       /**
+        * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
+        *
+        * @param int $maxage Maximum cache time on the Squid, in seconds
+        * @since 1.27
+        */
+       public function lowerCdnMaxage( $maxage ) {
+               $this->mCdnMaxageLimit = $this->min( $maxage, $this->mCdnMaxageLimit );
+               $this->setSquidMaxage( $this->mSquidMaxage );
        }
 
        /**
index 1df0ca0..4d72c24 100644 (file)
@@ -121,7 +121,7 @@ abstract class RevisionListBase extends ContextSource {
 
        /**
         * Do the DB query to iterate through the objects.
-        * @param IDatabase $db DatabaseBase object to use for the query
+        * @param IDatabase $db DB object to use for the query
         */
        abstract public function doQuery( $db );
 
index 64e5ea0..76e7f7e 100644 (file)
@@ -281,12 +281,12 @@ class SiteStatsInit {
 
        /**
         * Constructor
-        * @param bool|DatabaseBase $database
-        * - Boolean: whether to use the master DB
-        * - DatabaseBase: database connection to use
+        * @param bool|IDatabase $database
+        * - boolean: Whether to use the master DB
+        * - IDatabase: Database connection to use
         */
        public function __construct( $database = false ) {
-               if ( $database instanceof DatabaseBase ) {
+               if ( $database instanceof IDatabase ) {
                        $this->db = $database;
                } else {
                        $this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE );
@@ -365,10 +365,10 @@ class SiteStatsInit {
         * for the original initStats, but without output.
         *
         * @param IDatabase|bool $database
-        * - Boolean: whether to use the master DB
-        * - DatabaseBase: database connection to use
+        * - boolean: Whether to use the master DB
+        * - IDatabase: Database connection to use
         * @param array $options Array of options, may contain the following values
-        * - activeUsers Boolean: whether to update the number of active users (default: false)
+        * - activeUsers boolean: Whether to update the number of active users (default: false)
         */
        public static function doAllAndCommit( $database, array $options = array() ) {
                $options += array( 'update' => false, 'activeUsers' => false );
index e2cbc8e..8e5fae9 100644 (file)
@@ -4418,12 +4418,9 @@ class Title {
         * on the number of links. Typically called on create and delete.
         */
        public function touchLinks() {
-               $u = new HTMLCacheUpdate( $this, 'pagelinks' );
-               $u->doUpdate();
-
+               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
                if ( $this->getNamespace() == NS_CATEGORY ) {
-                       $u = new HTMLCacheUpdate( $this, 'categorylinks' );
-                       $u->doUpdate();
+                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
                }
        }
 
index 20b75bf..75649a7 100644 (file)
@@ -458,7 +458,7 @@ class User implements IDBAccessObject {
                $data['mVersion'] = self::VERSION;
                $key = wfMemcKey( 'user', 'id', $this->mId );
 
-               $opts = DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+               $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
                ObjectCache::getMainWANInstance()->set( $key, $data, 3600, $opts );
        }
 
index 0d1708f..3a3eb53 100644 (file)
@@ -146,7 +146,7 @@ class UserRightsProxy {
         *
         * @param string $database
         * @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
-        * @return DatabaseBase|null If invalid selection
+        * @return IDatabase|null If invalid selection
         */
        public static function getDB( $database, $ignoreInvalidDB = false ) {
                global $wgDBname;
index f402f3b..bd80c79 100644 (file)
@@ -1177,120 +1177,6 @@ HTML;
        }
 }
 
-/**
- * Object to access the $_FILES array
- */
-class WebRequestUpload {
-       protected $request;
-       protected $doesExist;
-       protected $fileInfo;
-
-       /**
-        * Constructor. Should only be called by WebRequest
-        *
-        * @param WebRequest $request The associated request
-        * @param string $key Key in $_FILES array (name of form field)
-        */
-       public function __construct( $request, $key ) {
-               $this->request = $request;
-               $this->doesExist = isset( $_FILES[$key] );
-               if ( $this->doesExist ) {
-                       $this->fileInfo = $_FILES[$key];
-               }
-       }
-
-       /**
-        * Return whether a file with this name was uploaded.
-        *
-        * @return bool
-        */
-       public function exists() {
-               return $this->doesExist;
-       }
-
-       /**
-        * Return the original filename of the uploaded file
-        *
-        * @return string|null Filename or null if non-existent
-        */
-       public function getName() {
-               if ( !$this->exists() ) {
-                       return null;
-               }
-
-               global $wgContLang;
-               $name = $this->fileInfo['name'];
-
-               # Safari sends filenames in HTML-encoded Unicode form D...
-               # Horrid and evil! Let's try to make some kind of sense of it.
-               $name = Sanitizer::decodeCharReferences( $name );
-               $name = $wgContLang->normalize( $name );
-               wfDebug( __METHOD__ . ": {$this->fileInfo['name']} normalized to '$name'\n" );
-               return $name;
-       }
-
-       /**
-        * Return the file size of the uploaded file
-        *
-        * @return int File size or zero if non-existent
-        */
-       public function getSize() {
-               if ( !$this->exists() ) {
-                       return 0;
-               }
-
-               return $this->fileInfo['size'];
-       }
-
-       /**
-        * Return the path to the temporary file
-        *
-        * @return string|null Path or null if non-existent
-        */
-       public function getTempName() {
-               if ( !$this->exists() ) {
-                       return null;
-               }
-
-               return $this->fileInfo['tmp_name'];
-       }
-
-       /**
-        * Return the upload error. See link for explanation
-        * http://www.php.net/manual/en/features.file-upload.errors.php
-        *
-        * @return int One of the UPLOAD_ constants, 0 if non-existent
-        */
-       public function getError() {
-               if ( !$this->exists() ) {
-                       return 0; # UPLOAD_ERR_OK
-               }
-
-               return $this->fileInfo['error'];
-       }
-
-       /**
-        * Returns whether this upload failed because of overflow of a maximum set
-        * in php.ini
-        *
-        * @return bool
-        */
-       public function isIniSizeOverflow() {
-               if ( $this->getError() == UPLOAD_ERR_INI_SIZE ) {
-                       # PHP indicated that upload_max_filesize is exceeded
-                       return true;
-               }
-
-               $contentLength = $this->request->getHeader( 'CONTENT_LENGTH' );
-               if ( $contentLength > wfShorthandToInteger( ini_get( 'post_max_size' ) ) ) {
-                       # post_max_size is exceeded
-                       return true;
-               }
-
-               return false;
-       }
-}
-
 /**
  * WebRequest clone which takes values from a provided array.
  *
diff --git a/includes/WebRequestUpload.php b/includes/WebRequestUpload.php
new file mode 100644 (file)
index 0000000..e743d9d
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Object to access the $_FILES array
+ *
+ * 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
+ */
+
+/**
+ * Object to access the $_FILES array
+ *
+ * @ingroup HTTP
+ */
+class WebRequestUpload {
+       protected $request;
+       protected $doesExist;
+       protected $fileInfo;
+
+       /**
+        * Constructor. Should only be called by WebRequest
+        *
+        * @param WebRequest $request The associated request
+        * @param string $key Key in $_FILES array (name of form field)
+        */
+       public function __construct( $request, $key ) {
+               $this->request = $request;
+               $this->doesExist = isset( $_FILES[$key] );
+               if ( $this->doesExist ) {
+                       $this->fileInfo = $_FILES[$key];
+               }
+       }
+
+       /**
+        * Return whether a file with this name was uploaded.
+        *
+        * @return bool
+        */
+       public function exists() {
+               return $this->doesExist;
+       }
+
+       /**
+        * Return the original filename of the uploaded file
+        *
+        * @return string|null Filename or null if non-existent
+        */
+       public function getName() {
+               if ( !$this->exists() ) {
+                       return null;
+               }
+
+               global $wgContLang;
+               $name = $this->fileInfo['name'];
+
+               # Safari sends filenames in HTML-encoded Unicode form D...
+               # Horrid and evil! Let's try to make some kind of sense of it.
+               $name = Sanitizer::decodeCharReferences( $name );
+               $name = $wgContLang->normalize( $name );
+               wfDebug( __METHOD__ . ": {$this->fileInfo['name']} normalized to '$name'\n" );
+               return $name;
+       }
+
+       /**
+        * Return the file size of the uploaded file
+        *
+        * @return int File size or zero if non-existent
+        */
+       public function getSize() {
+               if ( !$this->exists() ) {
+                       return 0;
+               }
+
+               return $this->fileInfo['size'];
+       }
+
+       /**
+        * Return the path to the temporary file
+        *
+        * @return string|null Path or null if non-existent
+        */
+       public function getTempName() {
+               if ( !$this->exists() ) {
+                       return null;
+               }
+
+               return $this->fileInfo['tmp_name'];
+       }
+
+       /**
+        * Return the upload error. See link for explanation
+        * http://www.php.net/manual/en/features.file-upload.errors.php
+        *
+        * @return int One of the UPLOAD_ constants, 0 if non-existent
+        */
+       public function getError() {
+               if ( !$this->exists() ) {
+                       return 0; # UPLOAD_ERR_OK
+               }
+
+               return $this->fileInfo['error'];
+       }
+
+       /**
+        * Returns whether this upload failed because of overflow of a maximum set
+        * in php.ini
+        *
+        * @return bool
+        */
+       public function isIniSizeOverflow() {
+               if ( $this->getError() == UPLOAD_ERR_INI_SIZE ) {
+                       # PHP indicated that upload_max_filesize is exceeded
+                       return true;
+               }
+
+               $contentLength = $this->request->getHeader( 'CONTENT_LENGTH' );
+               if ( $contentLength > wfShorthandToInteger( ini_get( 'post_max_size' ) ) ) {
+                       # post_max_size is exceeded
+                       return true;
+               }
+
+               return false;
+       }
+}
index 6215af1..325831e 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 /**
- * Helper tools for dealing with other locally-hosted wikis
+ * Helper tools for dealing with other wikis.
  */
 class WikiMap {
 
@@ -32,6 +32,20 @@ class WikiMap {
         * @return WikiReference|null WikiReference object or null if the wiki was not found
         */
        public static function getWiki( $wikiID ) {
+               $wikiReference = self::getWikiReferenceFromWgConf( $wikiID );
+               if ( $wikiReference ) {
+                       return $wikiReference;
+               }
+
+               // Try sites, if $wgConf failed
+               return self::getWikiWikiReferenceFromSites( $wikiID );
+       }
+
+       /**
+        * @param string $wikiID
+        * @return WikiReference|null WikiReference object or null if the wiki was not found
+        */
+       private static function getWikiReferenceFromWgConf( $wikiID ) {
                global $wgConf;
 
                $wgConf->loadFullData();
@@ -54,6 +68,42 @@ class WikiMap {
                return new WikiReference( $canonicalServer, $path, $server );
        }
 
+       /**
+        * @param string $wikiID
+        * @return WikiReference|null WikiReference object or null if the wiki was not found
+        */
+       private static function getWikiWikiReferenceFromSites( $wikiID ) {
+               static $siteStore = null;
+               if ( !$siteStore ) {
+                       // Replace once T114471 got fixed and don't do the caching here.
+                       $siteStore = SiteSQLStore::newInstance();
+               }
+
+               $site = $siteStore->getSite( $wikiID );
+
+               if ( !$site instanceof MediaWikiSite ) {
+                       // Abort if not a MediaWikiSite, as this is about Wikis
+                       return null;
+               }
+
+               $urlParts = wfParseUrl( $site->getPageUrl() );
+               if ( $urlParts === false || !isset( $urlParts['path'] ) || !isset( $urlParts['host'] ) ) {
+                       // We can't create a meaningful WikiReference without URLs
+                       return null;
+               }
+
+               // XXX: Check whether path contains a $1?
+               $path = $urlParts['path'];
+               if ( isset( $urlParts['query'] ) ) {
+                       $path .= '?' . $urlParts['query'];
+               }
+
+               $canonicalServer = isset( $urlParts['scheme'] ) ? $urlParts['scheme'] : 'http';
+               $canonicalServer .= '://' . $urlParts['host'];
+
+               return new WikiReference( $canonicalServer, $path );
+       }
+
        /**
         * Convenience to get the wiki's display name
         *
index 4e74ed3..78dd5fe 100644 (file)
@@ -682,7 +682,7 @@ class InfoAction extends FormlessAction {
                                $dbr = wfGetDB( DB_SLAVE );
                                $dbrWatchlist = wfGetDB( DB_SLAVE, 'watchlist' );
 
-                               $setOpts += DatabaseBase::getCacheSetOptions( $dbr, $dbrWatchlist );
+                               $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
 
                                $result = array();
 
index 057b011..12b9893 100644 (file)
@@ -41,18 +41,26 @@ class ApiQueryFileRepoInfo extends ApiQueryBase {
        }
 
        public function execute() {
+               $conf = $this->getConfig();
+
                $params = $this->extractRequestParams();
                $props = array_flip( $params['prop'] );
 
                $repos = array();
 
                $repoGroup = $this->getInitialisedRepoGroup();
+               $foreignTargets = $conf->get( 'ForeignUploadTargets' );
+
+               $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$repos, $props, $foreignTargets ) {
+                       $repoProps = $repo->getInfo();
+                       $repoProps['canUpload'] = in_array( $repoProps['name'], $foreignTargets );
 
-               $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$repos, $props ) {
-                       $repos[] = array_intersect_key( $repo->getInfo(), $props );
+                       $repos[] = array_intersect_key( $repoProps, $props );
                } );
 
-               $repos[] = array_intersect_key( $repoGroup->getLocalRepo()->getInfo(), $props );
+               $localInfo = $repoGroup->getLocalRepo()->getInfo();
+               $localInfo['canUpload'] = $conf->get( 'EnableUploads' );
+               $repos[] = array_intersect_key( $localInfo, $props );
 
                $result = $this->getResult();
                ApiResult::setIndexedTagName( $repos, 'repo' );
@@ -85,10 +93,14 @@ class ApiQueryFileRepoInfo extends ApiQueryBase {
                        $props = array_merge( $props, array_keys( $repo->getInfo() ) );
                } );
 
-               return array_values( array_unique( array_merge(
+               $propValues = array_values( array_unique( array_merge(
                        $props,
                        array_keys( $repoGroup->getLocalRepo()->getInfo() )
                ) ) );
+
+               $propValues[] = 'canUpload';
+
+               return $propValues;
        }
 
        protected function getExamplesMessages() {
index 32e0af5..78a1bd4 100644 (file)
@@ -24,7 +24,7 @@
        "apihelp-main-param-servedby": "Inclúa o nome do servidor que servía a solicitude nos resultados.",
        "apihelp-main-param-curtimestamp": "Incluir a marca de tempo actual no resultado.",
        "apihelp-main-param-origin": "Cando se accede á API usando unha petición AJAX entre-dominios (CORS), inicializar o parámetro co dominio orixe. Isto debe incluírse en calquera petición pre-flight, e polo tanto debe ser parte da petición URI (non do corpo POST). Debe coincidir exactamente cunha das orixes na cabeceira <code>Origin</code>, polo que ten que ser fixado a algo como <kbd>https://en.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parámetro non coincide coa cabeceira <code>Origin</code>, devolverase unha resposta 403. Se este parámetro coincide coa cabeceira <code>Origin</code> e a orixe está na lista branca, porase unha cabeceira <code>Access-Control-Allow-Origin</code>.",
-       "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. Pode consultarse unha lista de códigos en <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd>, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
+       "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd> devolve unha lista de códigos de lingua, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
        "apihelp-block-description": "Bloquear un usuario.",
        "apihelp-block-param-user": "Nome de usuario, dirección ou rango de IPs que quere bloquear.",
        "apihelp-block-param-expiry": "Tempo de caducidade. Pode ser relativo (p. ex.<kbd>5 meses</kbd> ou <kbd>2 semanas</kbd>) ou absoluto (p. ex. 2014-09-18T12:34:56Z</kbd>). Se se pon kbd>infinite</kbd>, <kbd>indefinite</kbd>, ou <kbd>never</kbd>, o bloqueo nunca caducará.",
        "apihelp-parse-param-pst": "Fai unha transformación antes de gardar a entrada antes de analizala. Válida unicamente para usar con texto.",
        "apihelp-parse-param-onlypst": "Facer unha transformación antes de gardar (PST) a entrada, pero sen analizala. Devolve o mesmo wikitexto, despois de que a PST foi aplicada. Só válida cando se usa con <var>$1text</var>.",
        "apihelp-parse-param-effectivelanglinks": "Inclúe ligazóns de idioma proporcionadas polas extensións (para usar con <kbd>$1prop=langlinks</kbd>).",
-       "apihelp-parse-param-section": "Recuperar unicamente o contido deste número de sección ou cando <kbd>new</kbd> xera unha nova sección.\n\nA sección <kbd>new</kbd> só é atendida cando se especifica <var>text</var>.",
+       "apihelp-parse-param-section": "Analizar unicamente o contido deste número de sección.\n\nCando <kbd>nova</kbd>, analiza <var>$1text</var> e <var>$1sectiontitle</var> como se fose a engadir unha nova sección da páxina.\n\n<kbd>novo</kbd> só se permite cando especifica <var>text</var>.",
        "apihelp-parse-param-sectiontitle": "Novo título de sección cando <var>section</var> é <kbd>new</kbd>.\n\nA diferenza da edición de páxinas, non se oculta no <var>summary</var> cando se omite ou está baleiro.",
        "apihelp-parse-param-disablelimitreport": "Omitir o informe de límite (\"Informe de límite NewPP\") da saída do analizador.",
        "apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> no seu lugar.",
        "apihelp-query+pagepropnames-description": "Listar os nomes de todas as propiedades de páxina usados na wiki.",
        "apihelp-query+pagepropnames-param-limit": "Máximo número de nomes a retornar.",
        "apihelp-query+pagepropnames-example-simple": "Obter os dez primeiros nomes de propiedade.",
-       "apihelp-query+pageprops-description": "Obter varias propiedades definidas no contido da páxina.",
-       "apihelp-query+pageprops-param-prop": "Listar só esas propiedades. Útil para verificar se unha páxina concreta usa unha propiedade de páxina determinada.",
+       "apihelp-query+pageprops-description": "Obter varias propiedades de páxina definidas no contido da páxina.",
+       "apihelp-query+pageprops-param-prop": "Listar só estas propiedades de páxina (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina usados). Útil para verificar se as páxinas usan unha determinada propiedade de páxina.",
        "apihelp-query+pageprops-example-simple": "Obter as propiedades para as páxinas <kbd>Main Page</kbd> e <kbd>MediaWiki</kbd>",
        "apihelp-query+pageswithprop-description": "Mostrar a lista de páxinas que empregan unha propiedade determinada.",
-       "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina pola que enumerar as páxinas.",
+       "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina para a que enumerar as páxinas  (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina en uso).",
        "apihelp-query+pageswithprop-param-prop": "Que información incluír:",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "Engade o ID da páxina.",
        "apihelp-query+pageswithprop-paramvalue-prop-title": "Engade o título e o ID do espazo de nomes da páxina.",
-       "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade da páxina.",
+       "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade de páxina.",
        "apihelp-query+pageswithprop-param-limit": "Máximo número de páxinas a retornar.",
        "apihelp-query+pageswithprop-param-dir": "En que dirección ordenar.",
        "apihelp-query+pageswithprop-example-simple": "Lista as dez primeiras páxinas que usan  <code>&#123;&#123;DISPLAYTITLE:&#125;&#125;</code>.",
        "apihelp-setnotificationtimestamp-example-page": "Restaurar o estado de notificación para a <kbd>Páxina Principal</kbd>.",
        "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixar o selo de tempo de notificación para a <kbd>Main page</kbd> de forma que todas as edicións dende o 1 se xaneiro de 2012 queden sen revisar.",
        "apihelp-setnotificationtimestamp-example-allpages": "Restaurar o estado de notificación para as páxinas no espazo de nomes de <kbd>{{ns:user}}</kbd>.",
+       "apihelp-stashedit-param-title": "Título da páxina que se está a editar.",
+       "apihelp-stashedit-param-section": "Número de selección. O <kbd>0</kbd> é para a sección superior, <kbd>novo</kbd> para unha sección nova.",
+       "apihelp-stashedit-param-sectiontitle": "Título para unha nova sección.",
        "apihelp-stashedit-param-text": "Contido da páxina.",
        "apihelp-stashedit-param-contentmodel": "Modelo de contido para o novo contido.",
        "apihelp-stashedit-param-contentformat": "Formato de serialización de contido utilizado para o texto de entrada.",
index d5b33ca..6ae334b 100644 (file)
        "apihelp-query+pageprops-example-simple": "Holl de Eijeschaffte för di Sigge „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">Main Page</kbd>“ un „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">MediaWiki</kbd>“.",
        "apihelp-query+pageswithprop-description": "Donn alle Sigge met bechtemmpte Sigge_Eijeschaff opleßte.",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "Deiht de Kännong vun de Sigge derbei.",
+       "apihelp-query+pageswithprop-paramvalue-prop-value": "Deiht der Wäät för de Eijeschaff vun dä Sigg derbei.",
        "apihelp-query+pageswithprop-param-limit": "De jrüüßte Zahl Sigge för ußzejävve.",
        "apihelp-query+pageswithprop-param-dir": "En wälsche Reihjefollsch opleßte.",
        "apihelp-query+pageswithprop-example-generator": "Holl zohsäzlejje Aanjahbe övver de eezde zehn Sigge, woh <code>_&#95;NOTOC_&#95;</code> dren vörkütt.",
index 8e9120b..b340d5e 100644 (file)
@@ -82,6 +82,7 @@
        "apihelp-parse-example-text": "Wikitext parseren.",
        "apihelp-parse-example-summary": "Een samenvatting parseren.",
        "apihelp-protect-example-protect": "Een pagina beveiligen",
+       "apihelp-stashedit-param-text": "Pagina-inhoud.",
        "api-help-flag-readrights": "Voor deze module zijn leesrechten nodig.",
        "api-help-flag-writerights": "Voor deze module zijn schrijfrechten nodig.",
        "api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
index dd7a21c..76d94e2 100644 (file)
@@ -28,7 +28,7 @@
        "apihelp-login-param-domain": "شپول (اختياري).",
        "apihelp-login-example-login": "ننوتل.",
        "apihelp-move-description": "يو مخ لېږدول.",
-       "apihelp-query+search-example-simple": "د <kbd>مانا</kbd> پلټل.",
+       "apihelp-query+search-example-simple": "د <kbd>meaning</kbd> پلټل.",
        "apihelp-query+search-example-text": "د <kbd>مانا</kbd> لپاره متنونه پلټل.",
        "apihelp-query+watchlist-paramvalue-prop-title": "د يو مخ سرليک ورگډوي.",
        "apihelp-tag-param-reason": "د بدلون سبب.",
index e1b9b27..5531245 100644 (file)
@@ -1091,7 +1091,7 @@ class ChangeTags {
                return ObjectCache::getMainWANInstance()->getWithSetCallback(
                        wfMemcKey( 'active-tags' ),
                        function ( $oldValue, &$ttl, array &$setOpts ) {
-                               $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+                               $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
 
                                // Ask extensions which tags they consider active
                                $extensionActive = array();
@@ -1135,7 +1135,7 @@ class ChangeTags {
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
                                $dbr = wfGetDB( DB_SLAVE );
 
-                               $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+                               $setOpts += Database::getCacheSetOptions( $dbr );
 
                                $tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', array(), $fname );
 
@@ -1160,7 +1160,7 @@ class ChangeTags {
                return ObjectCache::getMainWANInstance()->getWithSetCallback(
                        wfMemcKey( 'valid-tags-hook' ),
                        function ( $oldValue, &$ttl, array &$setOpts ) {
-                               $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+                               $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
 
                                $tags = array();
                                Hooks::run( 'ListDefinedTags', array( &$tags ) );
@@ -1221,7 +1221,7 @@ class ChangeTags {
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
                                $dbr = wfGetDB( DB_SLAVE, 'vslow' );
 
-                               $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+                               $setOpts += Database::getCacheSetOptions( $dbr );
 
                                $res = $dbr->select(
                                        'change_tag',
index f044002..b9d344f 100644 (file)
@@ -927,9 +927,9 @@ abstract class DatabaseBase implements IDatabase {
 
                $isWriteQuery = $this->isWriteQuery( $sql );
                if ( $isWriteQuery ) {
-                       if ( !$this->mDoneWrites ) {
-                               wfDebug( __METHOD__ . ': Writes done: ' .
-                                       DatabaseBase::generalizeSQL( $sql ) . "\n" );
+                       $reason = $this->getLBInfo( 'readOnlyReason' );
+                       if ( is_string( $reason ) ) {
+                               throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
                        }
                        # Set a flag indicating that writes have been done
                        $this->mDoneWrites = microtime( true );
index 928de61..6453854 100644 (file)
@@ -451,3 +451,9 @@ This may indicate a bug in the software.',
  */
 class DBUnexpectedError extends DBError {
 }
+
+/**
+ * @ingroup Database
+ */
+class DBReadOnlyError extends DBError {
+}
index e5fb094..a06d826 100644 (file)
@@ -211,6 +211,21 @@ abstract class LBFactory {
                $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
                        $ret = $ret || $lb->hasMasterChanges();
                } );
+
+               return $ret;
+       }
+
+       /**
+        * Detemine if any lagged slave connection was used
+        * @since 1.27
+        * @return bool
+        */
+       public function laggedSlaveUsed() {
+               $ret = false;
+               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
+                       $ret = $ret || $lb->laggedSlaveUsed();
+               } );
+
                return $ret;
        }
 
index a0ef753..3350d19 100644 (file)
@@ -546,6 +546,14 @@ class LoadBalancer {
                        $trxProf->recordConnection( $host, $dbname, $masterOnly );
                }
 
+               # Make master connections read only if in lagged slave mode
+               if ( $masterOnly && $this->getServerCount() > 1 && $this->getLaggedSlaveMode() ) {
+                       $conn->setLBInfo( 'readOnlyReason',
+                               'The database has been automatically locked ' .
+                               'while the slave database servers catch up to the master'
+                       );
+               }
+
                return $conn;
        }
 
@@ -1131,6 +1139,7 @@ class LoadBalancer {
        }
 
        /**
+        * @note This method will trigger a DB connection if not yet done
         * @return bool Whether the generic connection for reads is highly "lagged"
         */
        public function getLaggedSlaveMode() {
@@ -1140,6 +1149,15 @@ class LoadBalancer {
                return $this->mLaggedSlaveMode;
        }
 
+       /**
+        * @note This method will never cause a new DB connection
+        * @return bool Whether any generic connection used for reads was highly "lagged"
+        * @since 1.27
+        */
+       public function laggedSlaveUsed() {
+               return $this->mLaggedSlaveMode;
+       }
+
        /**
         * Disables/enables lag checks
         * @param null|bool $mode
index cc70960..83d5f4c 100644 (file)
@@ -116,7 +116,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * Get a slave database connection for the specified cluster
         *
         * @param string $cluster Cluster name
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getSlave( $cluster ) {
                global $wgDefaultExternalStore;
@@ -141,7 +141,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * Get a master database connection for the specified cluster
         *
         * @param string $cluster Cluster name
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getMaster( $cluster ) {
                $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
@@ -268,7 +268,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
         * Helper function for self::batchFetchBlobs for merging master/slave results
         * @param array &$ret Current self::batchFetchBlobs return value
         * @param array &$ids Map from blob_id to requested itemIDs
-        * @param mixed $res DB result from DatabaseBase::select
+        * @param mixed $res DB result from Database::select
         */
        private function mergeBatchResult( array &$ret, array &$ids, $res ) {
                foreach ( $res as $row ) {
index 59b2fd6..b6ddbad 100644 (file)
@@ -63,6 +63,9 @@ class FileBackendGroup {
        protected function initFromGlobals() {
                global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends;
 
+               // Register explicitly defined backends
+               $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
+
                $autoBackends = array();
                // Automatically create b/c backends for file repos...
                $repos = array_merge( $wgForeignFileRepos, array( $wgLocalFileRepo ) );
@@ -102,25 +105,18 @@ class FileBackendGroup {
                        );
                }
 
-               $backends = array_merge( $autoBackends, $wgFileBackends );
-
-               // Apply $wgReadOnly to all backends if not already read-only
-               foreach ( $backends as &$backend ) {
-                       $backend['readOnly'] = !empty( $backend['readOnly'] )
-                               ? $backend['readOnly']
-                               : wfConfiguredReadOnlyReason();
-               }
-
-               $this->register( $backends );
+               // Register implicitly defined backends
+               $this->register( $autoBackends, wfConfiguredReadOnlyReason() );
        }
 
        /**
         * Register an array of file backend configurations
         *
         * @param array $configs
+        * @param string|null $readOnlyReason
         * @throws FileBackendException
         */
-       protected function register( array $configs ) {
+       protected function register( array $configs, $readOnlyReason = null ) {
                foreach ( $configs as $config ) {
                        if ( !isset( $config['name'] ) ) {
                                throw new FileBackendException( "Cannot register a backend with no name." );
@@ -133,6 +129,10 @@ class FileBackendGroup {
                        }
                        $class = $config['class'];
 
+                       $config['readOnly'] = !empty( $config['readOnly'] )
+                               ? $config['readOnly']
+                               : $readOnlyReason;
+
                        unset( $config['class'] ); // backend won't need this
                        $this->backends[$name] = array(
                                'class' => $class,
index 4f64f02..0ade616 100644 (file)
@@ -27,7 +27,7 @@
  * @since 1.20
  */
 class DBFileJournal extends FileJournal {
-       /** @var DatabaseBase */
+       /** @var IDatabase */
        protected $dbw;
 
        protected $wiki = false; // string; wiki DB name
@@ -174,7 +174,7 @@ class DBFileJournal extends FileJournal {
        /**
         * Get a master connection to the logging DB
         *
-        * @return DatabaseBase
+        * @return IDatabase
         * @throws DBError
         */
        protected function getMasterDB() {
index b81cf3e..9d4f009 100644 (file)
@@ -146,7 +146,7 @@ abstract class DBLockManager extends QuorumLockManager {
         * Get (or reuse) a connection to a lock DB
         *
         * @param string $lockDb
-        * @return DatabaseBase
+        * @return IDatabase
         * @throws DBError
         */
        protected function getConnection( $lockDb ) {
@@ -185,10 +185,10 @@ abstract class DBLockManager extends QuorumLockManager {
         * Do additional initialization for new lock DB connection
         *
         * @param string $lockDb
-        * @param DatabaseBase $db
+        * @param IDatabase $db
         * @throws DBError
         */
-       protected function initConnection( $lockDb, DatabaseBase $db ) {
+       protected function initConnection( $lockDb, IDatabase $db ) {
        }
 
        /**
@@ -254,9 +254,9 @@ class MySqlLockManager extends DBLockManager {
 
        /**
         * @param string $lockDb
-        * @param DatabaseBase $db
+        * @param IDatabase $db
         */
-       protected function initConnection( $lockDb, DatabaseBase $db ) {
+       protected function initConnection( $lockDb, IDatabase $db ) {
                # Let this transaction see lock rows from other transactions
                $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
        }
index dfdb375..f3a560b 100644 (file)
@@ -72,7 +72,7 @@ class ForeignDBRepo extends LocalRepo {
        }
 
        /**
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getMasterDB() {
                if ( !isset( $this->dbConn ) ) {
@@ -84,7 +84,7 @@ class ForeignDBRepo extends LocalRepo {
        }
 
        /**
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getSlaveDB() {
                return $this->getMasterDB();
index f49b716..357f0b9 100644 (file)
@@ -53,14 +53,14 @@ class ForeignDBViaLBRepo extends LocalRepo {
        }
 
        /**
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getMasterDB() {
                return wfGetDB( DB_MASTER, array(), $this->wiki );
        }
 
        /**
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getSlaveDB() {
                return wfGetDB( DB_SLAVE, array(), $this->wiki );
index 389f081..02d859f 100644 (file)
@@ -205,7 +205,7 @@ class LocalRepo extends FileRepo {
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $that, $title ) {
                                $dbr = $that->getSlaveDB(); // possibly remote DB
 
-                               $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+                               $setOpts += Database::getCacheSetOptions( $dbr );
 
                                if ( $title instanceof Title ) {
                                        $row = $dbr->selectRow(
index 43d1eb5..588ae6b 100644 (file)
@@ -1430,8 +1430,7 @@ abstract class File implements IDBAccessObject {
                // Purge cache of all pages using this file
                $title = $this->getTitle();
                if ( $title ) {
-                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
-                       $update->doUpdate();
+                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
                }
        }
 
index d5179cb..390b7fe 100644 (file)
@@ -309,7 +309,7 @@ class LocalFile extends File {
 
                // Cache presence for 1 week and negatives for 1 day
                $ttl = $this->fileExists ? 86400 * 7 : 86400;
-               $opts = DatabaseBase::getCacheSetOptions( $this->repo->getSlaveDB() );
+               $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
                ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
        }
 
@@ -914,7 +914,7 @@ class LocalFile extends File {
         * Delete cached transformed files for the current version only.
         * @param array $options
         */
-       function purgeThumbnails( $options = array() ) {
+       public function purgeThumbnails( $options = array() ) {
                global $wgUseSquid;
 
                // Delete thumbnails
@@ -1428,8 +1428,9 @@ class LocalFile extends File {
                                $user
                        );
 
-                       $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
                        // Now that the page exists, make an RC entry.
+                       // This relies on the resetArticleID() call in WikiPage::insertOn(),
+                       // which is triggered on $descTitle by doEditContent() above.
                        $logEntry->publish( $logId );
                        if ( isset( $status->value['revision'] ) ) {
                                $dbw->update( 'logging',
@@ -1438,26 +1439,29 @@ class LocalFile extends File {
                                        __METHOD__
                                );
                        }
-                       $dbw->commit( __METHOD__ ); // commit before anything bad can happen
                }
 
-               if ( $reupload ) {
-                       # Delete old thumbnails
-                       $this->purgeThumbnails();
-
-                       # Remove the old file from the squid cache
-                       SquidUpdate::purge( array( $this->getURL() ) );
-               }
-
-               # Hooks, hooks, the magic of hooks...
-               Hooks::run( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
+               # Do some cache purges after final commit so that:
+               # a) Changes are more likely to be seen post-purge
+               # b) They won't cause rollback of the log publish/update above
+               $that = $this;
+               $dbw->onTransactionIdle( function () use ( $that, $reupload, $descTitle ) {
+                       # Run hook for other updates (typically more cache purging)
+                       Hooks::run( 'FileUpload', array( $that, $reupload, $descTitle->exists() ) );
+
+                       if ( $reupload ) {
+                               # Delete old thumbnails
+                               $that->purgeThumbnails();
+                               # Remove the old file from the squid cache
+                               SquidUpdate::purge( array( $that->getURL() ) );
+                       } else {
+                               # Update backlink pages pointing to this title if created
+                               LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
+                       }
+               } );
 
                # Invalidate cache for all pages using this file
-               $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
-               $update->doUpdate();
-               if ( !$reupload ) {
-                       LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
-               }
+               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
 
                return true;
        }
index fd92e11..42ee9e4 100644 (file)
@@ -364,9 +364,8 @@ class OldLocalFile extends LocalFile {
         * @param User $user User who did this upload
         * @return bool
         */
-       function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
+       protected function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
                $dbw = $this->repo->getMasterDB();
-               $dbw->begin( __METHOD__ );
 
                $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
                $props = $this->repo->getFileProps( $dstPath );
@@ -394,8 +393,6 @@ class OldLocalFile extends LocalFile {
                        ), __METHOD__
                );
 
-               $dbw->commit( __METHOD__ );
-
                return true;
        }
 
index 89aeaae..7a49f9b 100644 (file)
@@ -221,7 +221,7 @@ class Interwiki {
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
                                $dbr = wfGetDB( DB_SLAVE );
 
-                               $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+                               $setOpts += Database::getCacheSetOptions( $dbr );
 
                                $row = $dbr->selectRow(
                                        'interwiki',
index 1304362..7ce731d 100644 (file)
@@ -485,7 +485,7 @@ class JobRunner implements LoggerAwareInterface {
                // Re-ping all masters with transactions. This throws DBError if some
                // connection died while waiting on locks/slaves, triggering a rollback.
                wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
-                       $lb->forEachOpenConnection( function( DatabaseBase $conn ) use ( $fname ) {
+                       $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
                                if ( $conn->writesOrCallbacksPending() ) {
                                        $conn->query( "SELECT 1", $fname );
                                }
index a1de77e..4de19bc 100644 (file)
@@ -33,6 +33,7 @@ class AssembleUploadChunksJob extends Job {
        }
 
        public function run() {
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = RequestContext::importScopedSession( $this->params['session'] );
                $context = RequestContext::getMain();
                $user = $context->getUser();
@@ -53,7 +54,7 @@ class AssembleUploadChunksJob extends Job {
                        $upload->continueChunks(
                                $this->params['filename'],
                                $this->params['filekey'],
-                               $context->getRequest()
+                               new WebRequestUpload( $context->getRequest(), 'null' )
                        );
 
                        // Combine all of the chunks into a local file and upload that to a new stash file
@@ -104,7 +105,7 @@ class AssembleUploadChunksJob extends Job {
                                        'status' => Status::newFatal( 'api-error-stashfailed' )
                                )
                        );
-                       $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+                       $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
                        // To be extra robust.
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
 
index 8a180ec..59166e8 100644 (file)
@@ -35,6 +35,7 @@ class PublishStashedFileJob extends Job {
        }
 
        public function run() {
+               /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = RequestContext::importScopedSession( $this->params['session'] );
                $context = RequestContext::getMain();
                $user = $context->getUser();
@@ -120,7 +121,7 @@ class PublishStashedFileJob extends Job {
                                        'status' => Status::newFatal( 'api-error-publishfailed' )
                                )
                        );
-                       $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+                       $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
                        // To prevent potential database referential integrity issues.
                        // See bug 32551.
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
diff --git a/includes/libs/MemoizedCallable.php b/includes/libs/MemoizedCallable.php
new file mode 100644 (file)
index 0000000..14de6b9
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/**
+ * APC-backed function memoization
+ *
+ * This class provides memoization for pure functions. A function is pure
+ * if its result value depends on nothing other than its input parameters
+ * and if invoking it does not cause any side-effects.
+ *
+ * The first invocation of the memoized callable with a particular set of
+ * arguments will be delegated to the underlying callable. Repeat invocations
+ * with the same input parameters will be served from APC.
+ *
+ * @par Example:
+ * @code
+ * $memoizedStrrev = new MemoizedCallable( 'range' );
+ * $memoizedStrrev->invoke( 5, 8 );  // result: array( 5, 6, 7, 8 )
+ * $memoizedStrrev->invokeArgs( array( 5, 8 ) );  // same
+ * MemoizedCallable::call( 'range', array( 5, 8 ) );  // same
+ * @endcode
+ *
+ * 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
+ * @author Ori Livneh
+ * @since 1.27
+ */
+class MemoizedCallable {
+
+       /** @var callable */
+       private $callable;
+
+       /** @var string Unique name of callable; used for cache keys. */
+       private $callableName;
+
+       /**
+        * Constructor.
+        *
+        * @throws InvalidArgumentException if $callable is not a callable.
+        * @param callable $callable Function or method to memoize.
+        * @param int $ttl TTL in seconds. Defaults to 3600 (1hr). Capped at 86400 (24h).
+        */
+       public function __construct( $callable, $ttl = 3600 ) {
+               if ( !is_callable( $callable, false, $this->callableName ) ) {
+                       throw new InvalidArgumentException(
+                               'Argument 1 passed to MemoizedCallable::__construct() must ' .
+                               'be an instance of callable; ' . gettype( $callable ) . ' given'
+                       );
+               }
+
+               if ( $this->callableName === 'Closure::__invoke' ) {
+                       // Differentiate anonymous functions from one another
+                       $this->callableName .= uniqid();
+               }
+
+               $this->callable = $callable;
+               $this->ttl = min( max( $ttl, 1 ), 86400 );
+       }
+
+       /**
+        * Fetch the result of a previous invocation from APC.
+        *
+        * @param string $key
+        * @param bool &$success
+        */
+       protected function fetchResult( $key, &$success ) {
+               $success = false;
+               if ( function_exists( 'apc_fetch' ) ) {
+                       return apc_fetch( $key, $success );
+               }
+               return false;
+       }
+
+       /**
+        * Store the result of an invocation in APC.
+        *
+        * @param string $key
+        * @param mixed $result
+        */
+       protected function storeResult( $key, $result ) {
+               if ( function_exists( 'apc_store' ) ) {
+                       apc_store( $key, $result, $this->ttl );
+               }
+       }
+
+       /**
+        * Invoke the memoized function or method.
+        *
+        * @throws InvalidArgumentException If parameters list contains non-scalar items.
+        * @param array $args Parameters for memoized function or method.
+        * @return mixed The memoized callable's return value.
+        */
+       public function invokeArgs( Array $args = array() ) {
+               foreach ( $args as $arg ) {
+                       if ( $arg !== null && !is_scalar( $arg ) ) {
+                               throw new InvalidArgumentException(
+                                       'MemoizedCallable::invoke() called with non-scalar ' .
+                                       'argument'
+                               );
+                       }
+               }
+
+               $hash = md5( serialize( $args ) );
+               $key = __CLASS__ . ':' . $this->callableName . ':' . $hash;
+               $success = false;
+               $result = $this->fetchResult( $key, $success );
+               if ( !$success ) {
+                       $result = call_user_func_array( $this->callable, $args );
+                       $this->storeResult( $key, $result );
+               }
+
+               return $result;
+       }
+
+       /**
+        * Invoke the memoized function or method.
+        *
+        * Like MemoizedCallable::invokeArgs(), but variadic.
+        *
+        * @param mixed ...$params Parameters for memoized function or method.
+        * @return mixed The memoized callable's return value.
+        */
+       public function invoke() {
+               return $this->invokeArgs( func_get_args() );
+       }
+
+       /**
+        * Shortcut method for creating a MemoizedCallable and invoking it
+        * with the specified arguments.
+        *
+        * @param callable $callable
+        * @param array $args
+        * @param int $ttl
+        */
+       public static function call( $callable, Array $args = array(), $ttl = 3600 ) {
+               $instance = new self( $callable, $ttl );
+               return $instance->invokeArgs( $args );
+       }
+}
index 49966cf..c6fa914 100644 (file)
@@ -56,7 +56,7 @@ class MultiHttpClient {
        /** @var string|null proxy */
        protected $proxy;
        /** @var string */
-       protected $userAgent = 'MW-MultiHttpClient';
+       protected $userAgent = 'wikimedia/multi-http-client v1.0';
 
        /**
         * @param array $options
index 1cb544b..0b9aa7c 100644 (file)
@@ -45,7 +45,7 @@ class ObjectFactory {
         * Values in the arguments collection which are Closure instances will be
         * expanded by invoking them with no arguments before passing the
         * resulting value on to the constructor/callable. This can be used to
-        * pass DatabaseBase instances or other live objects to the
+        * pass IDatabase instances or other live objects to the
         * constructor/callable. This behavior can be suppressed by adding
         * closure_expansion => false to the specification.
         *
index 0dbbaba..522c5d7 100644 (file)
@@ -34,11 +34,9 @@ class APCBagOStuff extends BagOStuff {
         **/
        const KEY_SUFFIX = ':1';
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                $val = apc_fetch( $key . self::KEY_SUFFIX );
 
-               $casToken = $val;
-
                return $val;
        }
 
index 647d938..ecdf48f 100644 (file)
@@ -124,11 +124,35 @@ abstract class BagOStuff implements LoggerAwareInterface {
         * higher tiers using standard TTLs.
         *
         * @param string $key
-        * @param mixed $casToken [optional]
         * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
+        * @param integer $oldFlags [unused]
         * @return mixed Returns false on failure and if the item does not exist
         */
-       abstract public function get( $key, &$casToken = null, $flags = 0 );
+       public function get( $key, $flags = 0, $oldFlags = null ) {
+               // B/C for ( $key, &$casToken = null, $flags = 0 )
+               $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
+
+               return $this->doGet( $key, $flags );
+       }
+
+       /**
+        * @param string $key
+        * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
+        * @return mixed Returns false on failure and if the item does not exist
+        */
+       abstract protected function doGet( $key, $flags = 0 );
+
+       /**
+        * @note: This method is only needed if cas() is not is used for merge()
+        *
+        * @param string $key
+        * @param mixed $casToken
+        * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
+        * @return mixed Returns false on failure and if the item does not exist
+        */
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
+               throw new Exception( __METHOD__ . ' not implemented.' );
+       }
 
        /**
         * Set an item
@@ -182,7 +206,7 @@ abstract class BagOStuff implements LoggerAwareInterface {
                do {
                        $this->clearLastError();
                        $casToken = null; // passed by reference
-                       $currentValue = $this->get( $key, $casToken );
+                       $currentValue = $this->getWithToken( $key, $casToken, BagOStuff::READ_LATEST );
                        if ( $this->getLastError() ) {
                                return false; // don't spam retries (retry only on races)
                        }
@@ -237,7 +261,7 @@ abstract class BagOStuff implements LoggerAwareInterface {
                }
 
                $this->clearLastError();
-               $currentValue = $this->get( $key );
+               $currentValue = $this->get( $key, BagOStuff::READ_LATEST );
                if ( $this->getLastError() ) {
                        $success = false;
                } else {
index 55e84b0..bef0456 100644 (file)
@@ -27,7 +27,7 @@
  * @ingroup Cache
  */
 class EmptyBagOStuff extends BagOStuff {
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                return false;
        }
 
index b685e41..d4044e9 100644 (file)
@@ -48,7 +48,7 @@ class HashBagOStuff extends BagOStuff {
                return true;
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                if ( !isset( $this->bag[$key] ) ) {
                        return false;
                }
@@ -57,8 +57,6 @@ class HashBagOStuff extends BagOStuff {
                        return false;
                }
 
-               $casToken = $this->bag[$key][0];
-
                return $this->bag[$key][0];
        }
 
index 9e80e9f..9812047 100644 (file)
@@ -72,10 +72,10 @@ class ReplicatedBagOStuff extends BagOStuff {
                $this->readStore->setDebug( $debug );
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                return ( $flags & self::READ_LATEST )
-                       ? $this->writeStore->get( $key, $casToken, $flags )
-                       : $this->readStore->get( $key, $casToken, $flags );
+                       ? $this->writeStore->get( $key, $flags )
+                       : $this->readStore->get( $key, $flags );
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
index b1d3ec2..4beb627 100644 (file)
@@ -82,7 +82,7 @@ class WANObjectCache {
        /** Seconds to keep lock keys around */
        const LOCK_TTL = 5;
        /** Default remaining TTL at which to consider pre-emptive regeneration */
-       const LOW_TTL = 10;
+       const LOW_TTL = 30;
        /** Default time-since-expiry on a miss that makes a key "hot" */
        const LOCK_TSE = 1;
 
@@ -269,10 +269,12 @@ class WANObjectCache {
         *   - d) T1 reads the row and calls set() due to a cache miss
         *   - e) Stale value is stuck in cache
         *
+        * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
+        *
         * Example usage:
         * @code
         *     $dbr = wfGetDB( DB_SLAVE );
-        *     $setOpts = DatabaseBase::getCacheSetOptions( $dbr );
+        *     $setOpts = Database::getCacheSetOptions( $dbr );
         *     // Fetch the row from the DB
         *     $row = $dbr->selectRow( ... );
         *     $key = wfMemcKey( 'building', $buildingId );
@@ -505,6 +507,7 @@ class WANObjectCache {
         * can be set dynamically by altering $ttl in the callback (by reference).
         * The $setOpts array can be altered and is given to set() when called;
         * it is recommended to set the 'since' field to avoid race conditions.
+        * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
         *
         * Usually, callbacks ignore the current value, but it can be used
         * to maintain "most recent X" values that come from time or sequence
@@ -529,7 +532,7 @@ class WANObjectCache {
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             $dbr = wfGetDB( DB_SLAVE );
         *             // Account for any snapshot/slave lag
-        *             $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return $dbr->selectRow( ... );
         *        },
@@ -547,7 +550,7 @@ class WANObjectCache {
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             $dbr = wfGetDB( DB_SLAVE );
         *             // Account for any snapshot/slave lag
-        *             $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return CatConfig::newFromRow( $dbr->selectRow( ... ) );
         *        },
@@ -570,7 +573,7 @@ class WANObjectCache {
         *             // Determine new value from the DB
         *             $dbr = wfGetDB( DB_SLAVE );
         *             // Account for any snapshot/slave lag
-        *             $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return CatState::newFromResults( $dbr->select( ... ) );
         *        },
@@ -595,7 +598,7 @@ class WANObjectCache {
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             $dbr = wfGetDB( DB_SLAVE );
         *             // Account for any snapshot/slave lag
-        *             $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             // Start off with the last cached list
         *             $list = $oldValue ?: array();
index c480aa0..592565f 100644 (file)
  * @ingroup Cache
  */
 class WinCacheBagOStuff extends BagOStuff {
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
+               $casToken = null;
+
+               return $this->getWithToken( $key, $casToken, $flags );
+       }
+
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
                $val = wincache_ucache_get( $key );
 
                $casToken = $val;
index 9dbff6f..dc34f55 100644 (file)
@@ -28,7 +28,7 @@
  * @ingroup Cache
  */
 class XCacheBagOStuff extends BagOStuff {
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                $val = xcache_get( $key );
 
                if ( is_string( $val ) ) {
index 3c28c5f..49ce21c 100644 (file)
@@ -115,23 +115,17 @@ class UserMailer {
         * @return Status
         */
        public static function send( $to, $from, $subject, $body, $options = array() ) {
-               global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail;
+               global $wgAllowHTMLEmail;
                $contentType = 'text/plain; charset=UTF-8';
-               $headers = array();
-               if ( is_array( $options ) ) {
-                       $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
-                       $contentType = isset( $options['contentType'] ) ? $options['contentType'] : $contentType;
-                       $headers = isset( $options['headers'] ) ? $options['headers'] : $headers;
-               } else {
+               if ( !is_array( $options ) ) {
                        // Old calling style
                        wfDeprecated( __METHOD__ . ' with $replyto as 5th parameter', '1.26' );
-                       $replyto = $options;
+                       $options = array( 'replyTo' => $options );
                        if ( func_num_args() === 6 ) {
-                               $contentType = func_get_arg( 5 );
+                               $options['contentType'] = func_get_arg( 5 );
                        }
                }
 
-               $mime = null;
                if ( !is_array( $to ) ) {
                        $to = array( $to );
                }
@@ -178,6 +172,72 @@ class UserMailer {
                        return Status::newFatal( 'user-mail-no-addy' );
                }
 
+               // give a chance to UserMailerTransformContents subscribers who need to deal with each
+               // target differently to split up the address list
+               if ( count( $to ) > 1 ) {
+                       $oldTo = $to;
+                       Hooks::run( 'UserMailerSplitTo', array( &$to ) );
+                       if ( $oldTo != $to ) {
+                               $splitTo = array_diff( $oldTo, $to );
+                               $to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook
+                               // first send to non-split address list, then to split addresses one by one
+                               $status = Status::newGood();
+                               if ( $to ) {
+                                       $status->merge( UserMailer::sendInternal(
+                                               $to, $from, $subject, $body, $options ) );
+                               }
+                               foreach ( $splitTo as $newTo ) {
+                                       $status->merge( UserMailer::sendInternal(
+                                               array( $newTo ), $from, $subject, $body, $options ) );
+                               }
+                               return $status;
+                       }
+               }
+
+               return UserMailer::sendInternal( $to, $from, $subject, $body, $options );
+       }
+
+       /**
+        * Helper function fo UserMailer::send() which does the actual sending. It expects a $to
+        * list which the UserMailerSplitTo hook would not split further.
+        * @param MailAddress[] $to Array of recipients' email addresses
+        * @param MailAddress $from Sender's email
+        * @param string $subject Email's subject.
+        * @param string $body Email's text or Array of two strings to be the text and html bodies
+        * @param array $options:
+        *              'replyTo' MailAddress
+        *              'contentType' string default 'text/plain; charset=UTF-8'
+        *              'headers' array Extra headers to set
+        *
+        * @throws MWException
+        * @throws Exception
+        * @return Status
+        */
+       protected static function sendInternal(
+               array $to,
+               MailAddress $from,
+               $subject,
+               $body,
+               $options = array()
+       ) {
+               global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams;
+               $mime = null;
+
+               $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
+               $contentType = isset( $options['contentType'] ) ?
+                       $options['contentType'] : 'text/plain; charset=UTF-8';
+               $headers = isset( $options['headers'] ) ? $options['headers'] : array();
+
+               // Allow transformation of content, such as encrypting/signing
+               $error = false;
+               if ( !Hooks::run( 'UserMailerTransformContent', array( $to, $from, &$body, &$error ) ) ) {
+                       if ( $error ) {
+                               return Status::newFatal( 'php-mail-error', $error );
+                       } else {
+                               return Status::newFatal( 'php-mail-error-unknown' );
+                       }
+               }
+
                /**
                 * Forge email headers
                 * -------------------
@@ -276,6 +336,17 @@ class UserMailer {
                        $headers['Content-transfer-encoding'] = '8bit';
                }
 
+               // allow transformation of MIME-encoded message
+               if ( !Hooks::run( 'UserMailerTransformMessage',
+                       array( $to, $from, &$subject, &$headers, &$body, &$error ) )
+               ) {
+                       if ( $error ) {
+                               return Status::newFatal( 'php-mail-error', $error );
+                       } else {
+                               return Status::newFatal( 'php-mail-error-unknown' );
+                       }
+               }
+
                $ret = Hooks::run( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) );
                if ( $ret === false ) {
                        // the hook implementation will return false to skip regular mail sending
index 7d12749..412f017 100644 (file)
@@ -57,7 +57,13 @@ class MemcachedBagOStuff extends BagOStuff {
                return $params;
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
+               $casToken = null;
+
+               return $this->getWithToken( $key, $casToken, $flags );
+       }
+
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
                return $this->client->get( $this->encodeKey( $key ), $casToken );
        }
 
index 1b2c8db..a7b48a2 100644 (file)
@@ -115,7 +115,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                $this->client->addServers( $servers );
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
                $this->debugLog( "get($key)" );
                $result = $this->client->get( $this->encodeKey( $key ), null, $casToken );
                $result = $this->checkResult( $key, $result );
index b690772..c05ecb6 100644 (file)
@@ -87,11 +87,11 @@ class MultiWriteBagOStuff extends BagOStuff {
                $this->doWrite( self::ALL, 'setDebug', $debug );
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                $misses = 0; // number backends checked
                $value = false;
                foreach ( $this->caches as $cache ) {
-                       $value = $cache->get( $key, $casToken, $flags );
+                       $value = $cache->get( $key, $flags );
                        if ( $value !== false ) {
                                break;
                        }
index 7d9903f..e6b3f9e 100644 (file)
@@ -85,14 +85,13 @@ class RedisBagOStuff extends BagOStuff {
                }
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
                }
                try {
                        $value = $conn->get( $key );
-                       $casToken = $value;
                        $result = $this->unserialize( $value );
                } catch ( RedisException $e ) {
                        $result = false;
index 91189c8..c2e5bd7 100644 (file)
@@ -127,7 +127,7 @@ class SqlBagOStuff extends BagOStuff {
         * Get a connection to the specified database
         *
         * @param int $serverIndex
-        * @return DatabaseBase
+        * @return IDatabase
         * @throws MWException
         */
        protected function getDB( $serverIndex ) {
@@ -213,7 +213,13 @@ class SqlBagOStuff extends BagOStuff {
                }
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
+               $casToken = null;
+
+               return $this->getWithToken( $key, $casToken, $flags );
+       }
+
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
                $values = $this->getMulti( array( $key ) );
                if ( array_key_exists( $key, $values ) ) {
                        $casToken = $values[$key];
@@ -482,7 +488,7 @@ class SqlBagOStuff extends BagOStuff {
        }
 
        /**
-        * @param DatabaseBase $db
+        * @param IDatabase $db
         * @param string $exptime
         * @return bool
         */
@@ -491,7 +497,7 @@ class SqlBagOStuff extends BagOStuff {
        }
 
        /**
-        * @param DatabaseBase $db
+        * @param IDatabase $db
         * @return string
         */
        protected function getMaxDateTime( $db ) {
index bfcd4c3..c508abe 100644 (file)
@@ -169,8 +169,7 @@ class WikiFilePage extends WikiPage {
                $this->loadFile();
                if ( $this->mFile->exists() ) {
                        wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" );
-                       $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
-                       $update->doUpdate();
+                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ) );
                        $this->mFile->upgradeRow();
                        $this->mFile->purgeCache( array( 'forThumbRefresh' => true ) );
                } else {
index 98cc80a..e47e06c 100644 (file)
@@ -3221,8 +3221,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Images
                if ( $title->getNamespace() == NS_FILE ) {
-                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
-                       $update->doUpdate();
+                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
                }
 
                // User talk pages
index 7a5952f..f0e7f3e 100644 (file)
@@ -182,7 +182,7 @@ abstract class IndexPager extends ContextSource implements Pager {
        /**
         * Get the Database object in use
         *
-        * @return DatabaseBase
+        * @return IDatabase
         */
        public function getDatabase() {
                return $this->mDb;
index a637b93..0e53547 100644 (file)
@@ -924,9 +924,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                $this->missingLocalFileRefs[] = $file;
                        }
                }
-               return CSSMin::remap(
-                       $style, $localDir, $remoteDir, true
-               );
+               return MemoizedCallable::call( 'CSSMin::remap',
+                       array( $style, $localDir, $remoteDir, true ) );
        }
 
        /**
index 1857d23..eabafbd 100644 (file)
@@ -102,7 +102,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
                        'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
                        'wgResourceLoaderLegacyModules' => self::getLegacyModules(),
-                       'wgRemoteUploadTarget' => $conf->get( 'RemoteUploadTarget' ),
+                       'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
                );
 
                Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) );
index 1193bd6..17764a1 100644 (file)
@@ -34,22 +34,16 @@ class DBSiteStore implements SiteStore {
         */
        protected $sites = null;
 
-       /**
-        * @var ORMTable
-        */
-       protected $sitesTable;
-
        /**
         * @since 1.25
-        *
-        * @param ORMTable|null $sitesTable
+        * @param null $sitesTable Unused since 1.27
         */
-       public function __construct( ORMTable $sitesTable = null ) {
-               if ( $sitesTable === null ) {
-                       $sitesTable = $this->newSitesTable();
+       public function __construct( $sitesTable = null ) {
+               if ( $sitesTable !== null ) {
+                       throw new InvalidArgumentException(
+                               __METHOD__ . ': $sitesTable parameter must be null'
+                       );
                }
-
-               $this->sitesTable = $sitesTable;
        }
 
        /**
@@ -65,86 +59,6 @@ class DBSiteStore implements SiteStore {
                return $this->sites;
        }
 
-       /**
-        * Returns a new Site object constructed from the provided ORMRow.
-        *
-        * @since 1.25
-        *
-        * @param ORMRow $siteRow
-        *
-        * @return Site
-        */
-       protected function siteFromRow( ORMRow $siteRow ) {
-
-               $site = Site::newForType( $siteRow->getField( 'type', Site::TYPE_UNKNOWN ) );
-
-               $site->setGlobalId( $siteRow->getField( 'global_key' ) );
-
-               $site->setInternalId( $siteRow->getField( 'id' ) );
-
-               if ( $siteRow->hasField( 'forward' ) ) {
-                       $site->setForward( $siteRow->getField( 'forward' ) );
-               }
-
-               if ( $siteRow->hasField( 'group' ) ) {
-                       $site->setGroup( $siteRow->getField( 'group' ) );
-               }
-
-               if ( $siteRow->hasField( 'language' ) ) {
-                       $site->setLanguageCode( $siteRow->getField( 'language' ) === ''
-                               ? null
-                               : $siteRow->getField( 'language' )
-                       );
-               }
-
-               if ( $siteRow->hasField( 'source' ) ) {
-                       $site->setSource( $siteRow->getField( 'source' ) );
-               }
-
-               if ( $siteRow->hasField( 'data' ) ) {
-                       $site->setExtraData( $siteRow->getField( 'data' ) );
-               }
-
-               if ( $siteRow->hasField( 'config' ) ) {
-                       $site->setExtraConfig( $siteRow->getField( 'config' ) );
-               }
-
-               return $site;
-       }
-
-       /**
-        * Get a new ORMRow from a Site object
-        *
-        * @since 1.25
-        *
-        * @param Site $site
-        *
-        * @return ORMRow
-        */
-       protected function getRowFromSite( Site $site ) {
-               $fields = array(
-                       // Site data
-                       'global_key' => $site->getGlobalId(), // TODO: check not null
-                       'type' => $site->getType(),
-                       'group' => $site->getGroup(),
-                       'source' => $site->getSource(),
-                       'language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(),
-                       'protocol' => $site->getProtocol(),
-                       'domain' => strrev( $site->getDomain() ) . '.',
-                       'data' => $site->getExtraData(),
-
-                       // Site config
-                       'forward' => $site->shouldForward(),
-                       'config' => $site->getExtraConfig(),
-               );
-
-               if ( $site->getInternalId() !== null ) {
-                       $fields['id'] = $site->getInternalId();
-               }
-
-               return new ORMRow( $this->sitesTable, $fields );
-       }
-
        /**
         * Fetches the site from the database and loads them into the sites field.
         *
@@ -153,16 +67,46 @@ class DBSiteStore implements SiteStore {
        protected function loadSites() {
                $this->sites = new SiteList();
 
-               $siteRows = $this->sitesTable->select( null, array(), array(
-                       'ORDER BY' => 'site_global_key'
-               ) );
+               $dbr = wfGetDB( DB_SLAVE );
 
-               foreach ( $siteRows as $siteRow ) {
-                       $this->sites[] = $this->siteFromRow( $siteRow );
+               $res = $dbr->select(
+                       'sites',
+                       array(
+                               'site_id',
+                               'site_global_key',
+                               'site_type',
+                               'site_group',
+                               'site_source',
+                               'site_language',
+                               'site_protocol',
+                               'site_domain',
+                               'site_data',
+                               'site_forward',
+                               'site_config',
+                       ),
+                       '',
+                       __METHOD__,
+                       array( 'ORDER BY' => 'site_global_key' )
+               );
+
+               foreach ( $res as $row ) {
+                       $site = Site::newForType( $row->site_type );
+                       $site->setGlobalId( $row->site_global_key );
+                       $site->setInternalId( (int)$row->site_id );
+                       $site->setForward( (bool)$row->site_forward );
+                       $site->setGroup( $row->site_group );
+                       $site->setLanguageCode( $row->site_language === ''
+                               ? null
+                               : $row->site_language
+                       );
+                       $site->setSource( $row->site_source );
+                       $site->setExtraData( unserialize( $row->site_data ) );
+                       $site->setExtraConfig( unserialize( $row->site_config ) );
+                       $this->sites[] = $site;
                }
 
                // Batch load the local site identifiers.
-               $ids = wfGetDB( $this->sitesTable->getReadDb() )->select(
+               $ids = $dbr->select(
                        'site_identifiers',
                        array(
                                'si_site',
@@ -226,7 +170,7 @@ class DBSiteStore implements SiteStore {
                        return true;
                }
 
-               $dbw = $this->sitesTable->getWriteDbConnection();
+               $dbw = wfGetDB( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
 
@@ -240,12 +184,37 @@ class DBSiteStore implements SiteStore {
                                $internalIds[] = $site->getInternalId();
                        }
 
-                       $siteRow = $this->getRowFromSite( $site );
-                       $success = $siteRow->save( __METHOD__ ) && $success;
+                       $fields = array(
+                               // Site data
+                               'site_global_key' => $site->getGlobalId(), // TODO: check not null
+                               'site_type' => $site->getType(),
+                               'site_group' => $site->getGroup(),
+                               'site_source' => $site->getSource(),
+                               'site_language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(),
+                               'site_protocol' => $site->getProtocol(),
+                               'site_domain' => strrev( $site->getDomain() ) . '.',
+                               'site_data' => serialize( $site->getExtraData() ),
+
+                               // Site config
+                               'site_forward' => $site->shouldForward() ? 1 : 0,
+                               'site_config' => serialize( $site->getExtraConfig() ),
+                       );
+
+                       $rowId = $site->getInternalId();
+                       if ( $rowId !== null ) {
+                               $success = $dbw->update(
+                                       'sites', $fields, array( 'site_id' => $rowId ), __METHOD__
+                               ) && $success;
+                       } else {
+                               $rowId = $dbw->nextSequenceValue( 'sites_site_id_seq' );
+                               $fields['site_id'] = $rowId;
+                               $success = $dbw->insert( 'sites', $fields, __METHOD__ ) && $success;
+                               $rowId = $dbw->insertId();
+                       }
 
                        foreach ( $site->getLocalIds() as $idType => $ids ) {
                                foreach ( $ids as $id ) {
-                                       $localIds[] = array( $siteRow->getId(), $idType, $id );
+                                       $localIds[] = array( $rowId, $idType, $id );
                                }
                        }
                }
@@ -294,7 +263,7 @@ class DBSiteStore implements SiteStore {
         * @return bool Success
         */
        public function clear() {
-               $dbw = $this->sitesTable->getWriteDbConnection();
+               $dbw = wfGetDB( DB_MASTER );
 
                $dbw->startAtomic( __METHOD__ );
                $ok = $dbw->delete( 'sites', '*', __METHOD__ );
@@ -306,44 +275,4 @@ class DBSiteStore implements SiteStore {
                return $ok;
        }
 
-       /**
-        * @since 1.25
-        *
-        * @return ORMTable
-        */
-       protected function newSitesTable() {
-               return new ORMTable(
-                       'sites',
-                       array(
-                               'id' => 'id',
-
-                               // Site data
-                               'global_key' => 'str',
-                               'type' => 'str',
-                               'group' => 'str',
-                               'source' => 'str',
-                               'language' => 'str',
-                               'protocol' => 'str',
-                               'domain' => 'str',
-                               'data' => 'array',
-
-                               // Site config
-                               'forward' => 'bool',
-                               'config' => 'array',
-                       ),
-                       array(
-                               'type' => Site::TYPE_UNKNOWN,
-                               'group' => Site::GROUP_NONE,
-                               'source' => Site::SOURCE_LOCAL,
-                               'data' => array(),
-
-                               'forward' => false,
-                               'config' => array(),
-                               'language' => '',
-                       ),
-                       'ORMRow',
-                       'site_'
-               );
-       }
-
 }
index e3230ff..e61179b 100644 (file)
@@ -34,12 +34,18 @@ class SiteSQLStore extends CachingSiteStore {
         * @since 1.21
         * @deprecated 1.25 Construct a SiteStore instance directly instead.
         *
-        * @param ORMTable|null $sitesTable
+        * @param null $sitesTable Unused
         * @param BagOStuff|null $cache
         *
         * @return SiteStore
         */
-       public static function newInstance( ORMTable $sitesTable = null, BagOStuff $cache = null ) {
+       public static function newInstance( $sitesTable = null, BagOStuff $cache = null ) {
+               if ( $sitesTable !== null ) {
+                       throw new InvalidArgumentException(
+                               __METHOD__ . ': $sitesTable parameter is unused and must be null'
+                       );
+               }
+
                if ( $cache === null ) {
                        $cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING );
                }
index 91e84e4..fc7eeb1 100644 (file)
@@ -396,7 +396,7 @@ class SpecialSearch extends SpecialPage {
 
                $out->addHtml( "</div>" );
 
-               Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out ) );
+               Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out, $term ) );
 
        }
 
index 59350e6..07cb2bc 100644 (file)
@@ -26,7 +26,7 @@
 class BatchRowIterator implements RecursiveIterator {
 
        /**
-        * @var DatabaseBase $db The database to read from
+        * @var IDatabase $db The database to read from
         */
        protected $db;
 
@@ -58,7 +58,7 @@ class BatchRowIterator implements RecursiveIterator {
 
        /**
         * @var array $fetchColumns List of column names to select from the
-        *  table suitable for use with DatabaseBase::select()
+        *  table suitable for use with IDatabase::select()
         */
        protected $fetchColumns;
 
@@ -98,7 +98,7 @@ class BatchRowIterator implements RecursiveIterator {
 
        /**
         * @param array $condition Query conditions suitable for use with
-        *  DatabaseBase::select
+        *  IDatabase::select
         */
        public function addConditions( array $conditions ) {
                $this->conditions = array_merge( $this->conditions, $conditions );
@@ -106,7 +106,7 @@ class BatchRowIterator implements RecursiveIterator {
 
        /**
         * @param array $condition Query join conditions suitable for use
-        *  with DatabaseBase::select
+        *  with IDatabase::select
         */
        public function addJoinConditions( array $conditions ) {
                $this->joinConditions = array_merge( $this->joinConditions, $conditions );
@@ -114,7 +114,7 @@ class BatchRowIterator implements RecursiveIterator {
 
        /**
         * @param array $columns List of column names to select from the
-        *  table suitable for use with DatabaseBase::select()
+        *  table suitable for use with IDatabase::select()
         */
        public function setFetchColumns( array $columns ) {
                // If it's not the all column selector merge in the primary keys we need
index 377ed85..13cab5b 100644 (file)
@@ -22,7 +22,7 @@
  */
 class BatchRowWriter {
        /**
-        * @var DatabaseBase $db The database to write to
+        * @var IDatabase $db The database to write to
         */
        protected $db;
 
index 655c1d0..c866919 100644 (file)
@@ -57,7 +57,7 @@ class FileContentsHasher {
         * @return string|bool Hash of file contents, or false if the file could not be read.
         */
        public function getFileContentsHashInternal( $filePath, $algo = 'md4' ) {
-               $mtime = MediaWiki\quietCall( 'filemtime', $filePath );
+               $mtime = filemtime( $filePath );
                if ( $mtime === false ) {
                        return false;
                }
@@ -69,7 +69,7 @@ class FileContentsHasher {
                        return $hash;
                }
 
-               $contents = MediaWiki\quietCall( 'file_get_contents', $filePath );
+               $contents = file_get_contents( $filePath );
                if ( $contents === false ) {
                        return false;
                }
@@ -96,8 +96,12 @@ class FileContentsHasher {
                        $filePaths = (array)$filePaths;
                }
 
+               MediaWiki\suppressWarnings();
+
                if ( count( $filePaths ) === 1 ) {
-                       return $instance->getFileContentsHashInternal( $filePaths[0], $algo );
+                       $hash = $instance->getFileContentsHashInternal( $filePaths[0], $algo );
+                       MediaWiki\restoreWarnings();
+                       return $hash;
                }
 
                sort( $filePaths );
@@ -105,6 +109,8 @@ class FileContentsHasher {
                        return $instance->getFileContentsHashInternal( $filePath, $algo ) ?: '';
                }, $filePaths );
 
+               MediaWiki\restoreWarnings();
+
                $hashes = implode( '', $hashes );
                return $hashes ? hash( $algo, $hashes ) : false;
        }
index f3bd212..da9c10f 100644 (file)
        "changeemail-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
        "changeemail-oldemail": "Цяперашні адрас электроннай пошты:",
        "changeemail-newemail": "Новы адрас электроннай пошты:",
+       "changeemail-newemail-help": "Поле трэба пакінуць пустым, калі вы хочаце выдаліць свой адрас электроннай пошты. Пасьля выдаленьня вы ня зможаце ануляваць забыты пароль і ня будзеце атрымліваць лісты электроннай пошты з гэтай вікі.",
        "changeemail-none": "(няма)",
        "changeemail-password": "Ваш пароль у {{GRAMMAR:месны|{{SITENAME}}}}:",
        "changeemail-submit": "Зьмяніць адрас электроннай пошты",
        "nopagetext": "Пазначанай мэтавай старонкі не існуе.",
        "pager-newer-n": "$1 {{PLURAL:$1|навейшая|навейшыя|навейшых}}",
        "pager-older-n": "$1 {{PLURAL:$1|старэйшая|старэйшыя|старэйшых}}",
-       "suppress": "РÑ\8dвÑ\96заваÑ\86Ñ\8c",
+       "suppress": "Ð\9fадавÑ\96Ñ\86Ñ\8c Ð²Ñ\8dÑ\80Ñ\81Ñ\96Ñ\8e",
        "querypage-disabled": "Гэта спэцыяльная старонка адключаная для падвышэньня прадукцыйнасьці",
        "apihelp": "Даведка API",
        "apihelp-no-such-module": "Модуль «$1» ня знойдзены.",
index 241494e..8e935b7 100644 (file)
        "nstab-template": "обраꙁьць",
        "nstab-help": "страница помощи",
        "nstab-category": "катигорїꙗ",
+       "mainpage-nstab": "главьна страница",
        "nosuchspecialpage": "си нарочнꙑ страницѧ нѣстъ",
        "error": "блаꙁна",
        "internalerror": "вънѫтрѣнꙗ блаꙁна",
        "block-log-flags-anononly": "тъкъмо анѡнѷмьнꙑ польꙃєватєлє",
        "move-page": "прѣимєнованиѥ ⁖ $1 ⁖",
        "move-page-legend": "страницѧ прѣимєнованиѥ",
-       "movearticle": "страница :",
        "newtitle": "ново имѧ :",
        "move-watch": "си страницѧ блюдєниѥ",
        "movepagebtn": "прѣимєнованиѥ",
index 2c2770d..322426f 100644 (file)
        "nstab-template": "Şablon",
        "nstab-help": "Pela peşti",
        "nstab-category": "Kategoriye",
+       "mainpage-nstab": "Pela seri",
        "nosuchaction": "Fealiyeto wınasi çıniyo",
        "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
        "nosuchspecialpage": "Pela xasa wınasiye çıniya",
        "tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
        "tooltip-pt-logout": "Bıveciye",
        "tooltip-ca-talk": "Zerrekê pele sero werênayış",
-       "tooltip-ca-edit": "Tı şenay na pele bıvurnê. Kerem ke, qeydkerdış ra ver gocega verqayti bıgurene.",
+       "tooltip-ca-edit": "Ena pele bıvurne",
        "tooltip-ca-addsection": "Zu bınnusteya newi ak",
        "tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê",
        "tooltip-ca-history": "Versiyonê verênê ena pele",
        "api-error-badaccess-groups": "Ena wiki de dosya barkerdışi rê mısade nêdeyêno.",
        "api-error-badtoken": "Xetaya zerreki: Antışo xırabın.",
        "api-error-copyuploaddisabled": "URL barkerdış ena waster dı qefılyayo.",
-       "api-error-duplicate": "Ena {{PLURAL:$1|ze ke zeq|biya zey dosya da}} zeq wesiqa biya wendeyê.",
+       "api-error-duplicate": "Pele de xora be nê zerreki ra {{PLURAL:$1|dosyaya bine esta|dosyeyê bini estê}}.",
        "api-error-duplicate-archive": "Ena {{PLURAL:$1|vurneyaya zey na dosya|zerrey cı zey dosya}} aseno,feqet {{PLURAL:$1|ena dosya|tewr veri}} besterneyaya.",
        "api-error-empty-file": "Dosyaya ke şıma rışta venga.",
        "api-error-emptypage": "Newi, pelaya veng vıraştışi rê mısade nêdeyêno.",
index 2005ee8..2901393 100644 (file)
        "foreign-structured-upload-form-label-own-work": "This is my own work",
        "foreign-structured-upload-form-label-infoform-categories": "Categories",
        "foreign-structured-upload-form-label-infoform-date": "Date",
+       "foreign-structured-upload-form-label-own-work-message-local": "I confirm that I am uploading this file following the terms of service and licensing policies on {{SITENAME}}.",
+       "foreign-structured-upload-form-label-not-own-work-message-local": "If you are not able to upload this file under the policies of {{SITENAME}}, please close this dialog and try another method.",
+       "foreign-structured-upload-form-label-not-own-work-local-local": "You may also want to try [[Special:Upload|the default upload page]].",
        "foreign-structured-upload-form-label-own-work-message-default": "I understand that I am uploading this file to a shared repository. I confirm that I am doing so following the terms of service and licensing policies there.",
        "foreign-structured-upload-form-label-not-own-work-message-default": "If you are not able to upload this file under the policies of the shared repository, please close this dialog and try another method.",
        "foreign-structured-upload-form-label-not-own-work-local-default": "You may also want to try using [[Special:Upload|the upload page on {{SITENAME}}]], if this file can be uploaded there under their policies.",
index 0a1f8ee..f45db49 100644 (file)
        "broken-file-category": "Pages avec des liens de fichiers brisés",
        "about": "À propos",
        "article": "Page de contenu",
-       "newwindow": "(ouvre une nouvelle fenêtre)",
+       "newwindow": "(ouvre dans une nouvelle fenêtre)",
        "cancel": "Annuler",
        "moredotdotdot": "Plus...",
        "morenotlisted": "Cette liste n’est pas complète.",
index 83e65f8..2418fde 100644 (file)
        "viewsource": "Ver o código fonte",
        "viewsource-title": "Ver o código fonte de \"$1\"",
        "actionthrottled": "Acción limitada",
-       "actionthrottledtext": "Como unha medida de loita contra o ''spam'', limítase a realización desta acción a un número determinado de veces nun curto espazo de tempo, e vostede superou este límite.\nInténteo de novo nuns minutos.",
+       "actionthrottledtext": "Como medida contra os abusos, a acción que está realizando está limitada a un número determinado de veces nun periodo curto de tempo, e superou ese límite.\nInténteo de novo nuns minutos.",
        "protectedpagetext": "Esta páxina foi protexida para evitar a edición e outras accións.",
        "viewsourcetext": "Pode ver e copiar o código fonte desta páxina.",
        "viewyourtext": "Pode ver e copiar o código fonte <strong>das súas edicións</strong> nesta páxina.",
        "createacct-captcha": "Comprobación de seguridade",
        "createacct-imgcaptcha-ph": "Insira o texto que ve enriba",
        "createacct-submit": "Crear a conta",
-       "createacct-another-submit": "Crear outra conta",
+       "createacct-another-submit": "Crear conta",
        "createacct-benefit-heading": "Xente coma vostede elabora {{SITENAME}}.",
        "createacct-benefit-body1": "{{PLURAL:$1|edición|edicións}}",
        "createacct-benefit-body2": "{{PLURAL:$1|páxina|páxinas}}",
        "passwordreset-emailtext-ip": "Alguén (probablemente vostede, desde o enderezo IP $1) solicitou o restablecemento do seu\ncontrasinal de {{SITENAME}} ($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
        "passwordreset-emailtext-user": "O usuario $1 solicitou o restablecemento do contrasinal de {{SITENAME}}\n($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
        "passwordreset-emailelement": "Nome de usuario: \n$1\n\nContrasinal temporal: \n$2",
-       "passwordreset-emailsent": "Enviouse o correo electrónico de restablecemento do contrasinal.",
+       "passwordreset-emailsent": "Se esta é unha dirección de correo electrónico rexistrada para a túa conta, entón enviarase un correo electrónico para o restablecemento do teu contrasinal.",
        "passwordreset-emailsent-capture": "Enviouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación.",
        "passwordreset-emailerror-capture": "Xerouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación, pero o envío {{GENDER:$2|ao usuario|á usuaria}} fallou: $1",
-       "changeemail": "Cambiar o enderezo de correo electrónico",
-       "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio.",
+       "changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
+       "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio. Se vostede quere eliminar a asociación da dirección de correo electrónico da súa conta, deixe en branco a nova dirección de correo electrónico cando envíe o formulario.",
        "changeemail-no-info": "Debe rexistrarse para acceder directamente a esta páxina.",
        "changeemail-oldemail": "Enderezo de correo electrónico actual:",
        "changeemail-newemail": "Novo enderezo de correo electrónico:",
        "permissionserrorstext-withaction": "Non ten os permisos necesarios para $2, {{PLURAL:$1|pola seguinte razón|polas seguintes razóns}}:",
        "recreate-moveddeleted-warn": "'''Atención: Vai volver crear unha páxina que xa foi eliminada anteriormente.'''\n\nDebería considerar se é apropiado continuar a editar esta páxina.\nVelaquí están o rexistro de borrados e mais o de traslados desta páxina, por se quere consultalos:",
        "moveddeleted-notice": "Esta páxina foi borrada.\nA continuación pódese ver o rexistro de borrados e traslados desta páxina, por se quere consultalos.",
+       "moveddeleted-notice-recent": "Sentímolo, esta página foi borrada recentemente (dentro das últimas 24 horas).\nO rexistro de borrado e traslado da páxina amósanse abaixo como referencia.",
        "log-fulllog": "Ver o rexistro completo",
        "edit-hook-aborted": "A edición foi abortada polo asociador.\nEste non deu ningunha explicación.",
        "edit-gone-missing": "Non se pode actualizar a páxina.\nSemella que foi borrada.",
        "mergehistory-go": "Mostrar as edicións que se poden fusionar",
        "mergehistory-submit": "Fusionar as revisións",
        "mergehistory-empty": "Non hai revisións que se poidan fusionar.",
-       "mergehistory-done": "{{PLURAL:$3|Unha revisión|$3 revisións}} de \"$1\" {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con \"[[:$2]]\".",
+       "mergehistory-done": "$3 {{PLURAL:$3|revisión|revisións}} de $1 {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con [[:$2]].",
        "mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e data.",
        "mergehistory-fail-toobig": "Non se puido fusionar o historial, xa que supón trasladar máis revisións que o límite de $1 {{PLURAL:$1|revisión|revisións}}.",
        "mergehistory-no-source": "Non existe a páxina de orixe \"$1\".",
        "prefs-watchlist-token": "Pase para a lista de vixilancia:",
        "prefs-misc": "Preferencias varias",
        "prefs-resetpass": "Cambiar o contrasinal",
-       "prefs-changeemail": "Cambiar o enderezo de correo electrónico",
+       "prefs-changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
        "prefs-setemail": "Establecer un enderezo de correo electrónico",
        "prefs-email": "Opcións de correo electrónico",
        "prefs-rendering": "Aparencia",
        "group-bot": "Bots",
        "group-sysop": "Administradores",
        "group-bureaucrat": "Burócratas",
-       "group-suppress": "Supervisores",
+       "group-suppress": "Supresores",
        "group-all": "(todos)",
        "group-user-member": "{{GENDER:$1|usuario|usuaria}}",
        "group-autoconfirmed-member": "{{GENDER:$1|usuario autoconfirmado|usuaria autoconfirmada}}",
        "group-bot-member": "{{GENDER:$1|bot}}",
        "group-sysop-member": "{{GENDER:$1|administrador|administradora}}",
        "group-bureaucrat-member": "{{GENDER:$1|burócrata}}",
-       "group-suppress-member": "{{GENDER:$1|supervisor|supervisora}}",
+       "group-suppress-member": "{{GENDER:$1|supresor|supresora}}",
        "grouppage-user": "{{ns:project}}:Usuarios",
        "grouppage-autoconfirmed": "{{ns:project}}:Usuarios autoconfirmados",
        "grouppage-bot": "{{ns:project}}:Bots",
        "grouppage-sysop": "{{ns:project}}:Administradores",
        "grouppage-bureaucrat": "{{ns:project}}:Burócratas",
-       "grouppage-suppress": "{{ns:project}}:Supervisores",
+       "grouppage-suppress": "{{ns:project}}:Supresores",
        "right-read": "Ler páxinas",
        "right-edit": "Editar páxinas",
        "right-createpage": "Crear páxinas (que non son de conversa)",
        "recentchanges-page-added-to-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} engadíronse á categoría",
        "recentchanges-page-removed-from-category": "\"[[:$1]]\" eliminouse da categoría",
        "recentchanges-page-removed-from-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} elimináronse da categoría",
+       "autochange-username": "Cambio automático de MediaWiki",
        "upload": "Subir un ficheiro",
        "uploadbtn": "Subir un ficheiro",
        "reuploaddesc": "Cancelar a carga e volver ao formulario de carga",
        "upload-options": "Opcións de carga",
        "watchthisupload": "Vixiar este ficheiro",
        "filewasdeleted": "Un ficheiro con ese nome foi cargado con anterioridade e a continuación borrado.\nDebe comprobar o $1 antes de proceder a cargalo outra vez.",
+       "filename-thumb-name": "Semella que este título é dunha miniatura. Non cargue miniaturas no wiki do que as sacou. Se non é así, corrixa o nome do ficheiro para que sexa máis significativo e non teña o prefixo das miniaturas.",
        "filename-bad-prefix": "O nome do ficheiro que está cargando comeza con '''\"$1\"''', que é un típico nome non descritivo asignado automaticamente polas cámaras dixitais.\nPor favor, escolla un nome máis descritivo para o seu ficheiro.",
        "filename-prefix-blacklist": " #<!-- Deixe esta liña tal e como está --> <pre>\n# A sintaxe é a seguinte:\n#   * Todo o que vaia despois dun carácter \"#\" ata o final da liña é un comentario\n#   * Toda liña que non estea en branco é un prefixo para os nomes típicos dos ficheiros asignados automaticamente polas cámaras dixitais\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # algúns teléfonos móbiles\nIMG # xenérico\nJD # Jenoptik\nMGP # Pentax\nPICT # varios\n #</pre> <!-- Deixe esta liña tal e como está -->",
        "upload-success-subj": "A carga realizouse correctamente",
        "upload-form-label-infoform-description": "Descrición",
        "upload-form-label-usage-title": "Uso",
        "upload-form-label-usage-filename": "Nome do ficheiro",
+       "foreign-structured-upload-form-label-own-work": "Isto é o meu propio traballo",
+       "foreign-structured-upload-form-label-infoform-categories": "Categorías",
+       "foreign-structured-upload-form-label-infoform-date": "Data",
        "backend-fail-stream": "Non se puido transmitir o ficheiro \"$1\".",
        "backend-fail-backup": "Non se puido facer unha copia de seguridade do ficheiro \"$1\".",
        "backend-fail-notexists": "O ficheiro \"$1\" non existe.",
        "nopagetext": "A páxina que especificou non existe.",
        "pager-newer-n": "{{PLURAL:$1|unha posterior|$1 posteriores}}",
        "pager-older-n": "{{PLURAL:$1|unha anterior|$1 anteriores}}",
-       "suppress": "Supervisor",
+       "suppress": "Supresor",
        "querypage-disabled": "Esta páxina especial está desactivada por razóns de rendemento.",
        "apihelp": "Axuda coa API",
        "apihelp-no-such-module": "Non se atopou o módulo \"$1\".",
        "emailccsubject": "Copia da súa mensaxe para $1: $2",
        "emailsent": "Mensaxe enviada",
        "emailsenttext": "A súa mensaxe de correo electrónico foi enviada.",
-       "emailuserfooter": "Este correo electrónico foi enviado por $1 a $2 mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
+       "emailuserfooter": "Este correo electrónico foi {{GENDER:$1|enviado}} por $1 a {{GENDER:$2|$2}} mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
        "usermessage-summary": "Mensaxe deixada polo sistema.",
        "usermessage-editor": "Editor das mensaxes do sistema",
        "watchlist": "Lista de vixilancia",
        "deletepage": "Borrar a páxina",
        "confirm": "Confirmar",
        "excontent": "o contido era: \"$1\"",
-       "excontentauthor": "o contido era: \"$1\" (e o único editor foi [[Special:Contributions/$2|$2]])",
+       "excontentauthor": "o contido era: \"$1\", e o único editor foi \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|conversa]])",
        "exbeforeblank": "o contido antes do baleirado era: \"$1\"",
        "delete-confirm": "Borrar \"$1\"",
        "delete-legend": "Borrar",
        "move-page-legend": "Mover unha páxina",
        "movepagetext": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nPode actualizar automaticamente as redireccións que van dar ao título orixinal.\nSe escolle non facelo, asegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
        "movepagetext-noredirectfixer": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nAsegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
-       "movepagetalktext": "A páxina de conversa asociada, se existe, será automaticamente movida con esta '''agás que''':\n*Estea a mover a páxina empregando espazos de nomes,\n*Xa exista unha páxina de conversa con ese nome, ou\n*Desactive a opción de abaixo.\n\nNestes casos, terá que mover ou mesturar a páxina manualmente se o desexa.",
+       "movepagetalktext": "Se marca esta caixa, a páxina de conversa asociada trasladarase automáticamente ó título novo a menos que xa exista unha páxina de conversa non baleira alí.\n\nNeste caso, deberá trasladar ou fusionar manualmente a páxina se así o quere.",
        "moveuserpage-warning": "'''Aviso:''' Está a piques de mover unha páxina de usuario. Por favor, teña en conta que só se trasladará a páxina e que o usuario '''non''' será renomeado.",
        "movecategorypage-warning": "'''Aviso:''' Está a piques de mover unha páxina de categoría. Por favor, teña en conta que só se trasladará a páxina e que as páxinas presentes na categoría vella '''non''' serán recategorizadas na categoría nova.",
        "movenologintext": "Debe ser un usuario rexistrado e [[Special:UserLogin|acceder ao sistema]] para mover unha páxina.",
        "cant-move-to-user-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de usuario (agás a unha subpáxina).",
        "cant-move-category-page": "Non ten os permisos necesarios para mover páxinas de categoría.",
        "cant-move-to-category-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de categoría.",
-       "newtitle": "Ao novo título:",
+       "newtitle": "Novo título:",
        "move-watch": "Vixiar esta páxina",
        "movepagebtn": "Mover a páxina",
        "pagemovedsub": "O movemento foi un éxito",
        "logentry-newusers-byemail": "$1 {{GENDER:$2|creou}} a conta de usuario $3; o contrasinal enviouse por correo electrónico",
        "logentry-newusers-autocreate": "A conta de {{GENDER:$2|usuario|usuaria}} $1 creouse automaticamente",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|trasladou}} a protección de \"$4\" a \"$3\"",
+       "logentry-protect-unprotect": "$1 {{GENDER:$2|eliminou}} a protección de $3",
+       "logentry-protect-protect": "$1 {{GENDER:$2|protexeu}} a $3 $4",
+       "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protexeu}} a $3 $4 [en cascada]",
+       "logentry-protect-modify": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4",
+       "logentry-protect-modify-cascade": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4 [en cascada]",
        "logentry-rights-rights": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3 de $4 a $5",
        "logentry-rights-rights-legacy": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3",
        "logentry-rights-autopromote": "$1 foi {{GENDER:$2|promovido|promovida}} automaticamente de $4 a $5",
index 1c8a949..6cbf7eb 100644 (file)
        "tooltip-rollback": "Mbalèkaké suntingan-suntingan ing kaca iki menyang kontributor pungkasan nganggo sak klik.",
        "tooltip-undo": "Mbalèkaké révisi iki lan mbukak kothak panyuntingan jroning mode pratayang. Wènèhi kasempatan kanggo ngisi alesan ing kothak ringkesan.",
        "tooltip-preferences-save": "Simpen préperensi",
-       "tooltip-summary": "Lebkaké ringkesan cedhèk",
+       "tooltip-summary": "Lebokna ringkesan cendhèk",
        "anonymous": "{{PLURAL:$1|Panganggo|panganggo}} anon ing {{SITENAME}}.",
        "siteuser": "Panganggo {{SITENAME}} $1",
        "anonuser": "Panganggo anonim {{SITENAME}} $1",
index 3c575b5..f01ea6a 100644 (file)
        "undo-failure": "Dat kunnt mer nit zeröck nämme, dä Afschnedd wood enzwesche ald widder beärbeidt.",
        "undo-norev": "Do kam_mer nix zeröck nämme. Di väsjohn jidd_et nit, udder se es verschtoche udder fottjeschmeße woode.",
        "undo-nochange": "Di Änderong schingk ald retuur jemaat woode ze sin.",
-       "undo-summary": "De Änderong $1 fum [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
+       "undo-summary": "Di Änderong $1 wood {{GENDER:$2|vum|vum|vumm Metmaacher|vun dä|vum}} [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
        "undo-summary-username-hidden": "Nemm di Väsjohn $1 vun enem verschtoche Metmaacher widder retuhr.",
        "cantcreateaccounttitle": "Kann keine Zojang enrichte",
        "cantcreateaccount-text": "Dä [[User:$3|$3]] hät verbodde, dat mer sich vun dä IP-Adress '''$1''' uß als ene neue Metmaacher aanmelde könne soll.\n\nAls Jrund för et Sperre es enjedraare: ''$2''",
        "recentchangeslinked-summary": "Heh di {{int:nstab-special}} hädd en Leß met Änderonge aan Sigge, di vun dä aanjejovve Sigg uß verlengk sin.\nBei Saachjroppe sen et de Sigge en dä Saachjropp.\nSigge uß Dinge [[Special:Watchlist|Opaßleß]] sin en '''Fättschreff''' jeschrevve.",
        "recentchangeslinked-page": "Dä Sigg ier Övverschreff:",
        "recentchangeslinked-to": "Zeisch de Änderonge aan dä Sigge, woh Lengks op di aanjejovve Sigg drop sin",
+       "autochange-username": "Automattesche Ännderong aam MediaWiki",
        "upload": "Daate huhlade",
        "uploadbtn": "Huhlade!",
        "reuploaddesc": "Zeröck noh de Sigg zem Huhlade.",
        "cant-move-to-user-page": "Do häs nit dat Rääsch, en Sigg tirkäk op en Metmaacher-Sigg ömzenänne, Do kanns se ävver op en Ungersigg dofun ömnenne.",
        "cant-move-category-page": "Do häß nit dat Rääsch, Saachjroppesigge ömzebenänne.",
        "cant-move-to-category-page": "Do häß nit dat Rääsch, en Sigg obb en Saachjroppesigg ömzebenänne.",
-       "newtitle": "op dä neue Nahme",
+       "newtitle": "Dä neuje Nahme:",
        "move-watch": "Op di Sigg heh oppaßße",
        "movepagebtn": "Ömnenne",
        "pagemovedsub": "Dat Ömnenne hät jeflupp",
index f00d523..85ae81f 100644 (file)
        "cancel": "Ticcāhuaz",
        "moredotdotdot": "Huehca ōmpa...",
        "mypage": "Noāmauh",
-       "mytalk": "Notēixnāmiquiliz",
+       "mytalk": "Nozānīl",
        "anontalk": "Inīn IP ītēixnāmiquiliz",
        "navigation": "Nènemòwalistli",
        "and": "&#32;īhuān",
-       "qbfind": "Tlatēmōz",
-       "qbbrowse": "Titlatēmōz",
+       "qbfind": "Ticahciz",
+       "qbbrowse": "Titlatepotztocaz",
        "qbedit": "Ticpatlaz",
-       "qbpageoptions": "Inīn zāzanilli",
-       "qbmyoptions": "Nozāzanil",
+       "qbpageoptions": "Inīn tlaīxtli",
+       "qbmyoptions": "Notlaīx",
        "faq": "Zan īc tētlatlanīliztli",
        "faqpage": "Project:FAQ",
        "actions": "Āyiliztli",
-       "namespaces": "Tòkâyeyàntìn",
+       "namespaces": "Tōcātlacāuhtli",
        "errorpagetitle": "Aiuhcāyōtl",
        "returnto": "Timocuepāz īhuīc $1.",
        "tagline": "Īhuīcpa {{SITENAME}}",
        "help": "Tēpalēhuiliztli",
-       "search": "Tlatēmōz",
-       "searchbutton": "Tlatēmōz",
-       "go": "Yāuh",
-       "searcharticle": "Yāuh",
+       "search": "Titlatēmōz",
+       "searchbutton": "Tictēmōz",
+       "go": "Tiyāz",
+       "searcharticle": "Tiyāz",
        "history": "Tlaīxtli ītlahtōllo",
        "history_short": "Tlahtōllōtl",
        "updatedmarker": "ōmoyancuīx īhuīcpa xōcoyōc notlahpololiz",
        "printableversion": "Tepoztlahcuilōlli",
        "permalink": "Mochipa tzonhuiliztli",
        "print": "Tictepoztlahcuilōz",
-       "view": "Mà mỏta",
+       "view": "Tiquittaz",
+       "view-foreign": "Īpan tiquittaz in $1",
        "edit": "Ticpatlaz",
        "edit-local": "Ticpatlaz nicān tlahtōlli",
        "create": "Ticchīhuaz",
        "undeletethispage": "Ticmāquīxtīz inīn tlaīxtli",
        "undelete_short": "Ahticpolōz {{PLURAL:$1|cē tlapatlaliztli|$1 tlapatlaliztli}}",
        "viewdeleted_short": "Mà mỏta {{PLURAL:$1|se tlatlaìxpôpolòlli tlayèktlàlilistli|$1 tlatlaìxpôpolòltin tlayèktlàlilistin}}",
-       "protect": "Ticquīxtīz",
+       "protect": "Ticpiyaz",
        "protect_change": "ticpatlaz",
-       "protectthispage": "Ticquīxtiāz inīn zāzanilli",
-       "unprotect": "Ticpatlaz in tlaquīxtīliztli",
-       "unprotectthispage": "Ticpatlaz inīn āmatl ītlaquīxtīliz",
+       "protectthispage": "Ticpiyaz inīn tlaīxtli",
+       "unprotect": "Ticpatlaz in tlapiyaliztli",
+       "unprotectthispage": "Ticpatlaz inīn tlaīxtli ītlapiyaliz",
        "newpage": "Yancuic tlaīxtli",
        "talkpage": "Tictlahtōz inīn zāzaniltechcopa",
        "talkpagelinktext": "Zānīlli",
        "badaccess": "Tlahuelītiliztechcopa ahcuallōtl",
        "badaccess-group0": "Tehhuātl ahmo tiquichīhua inōn tiquiēlēhuia.",
        "badaccess-groups": "Inōn tiquiēlēhuia zan quichīhuah tlatequitiltilīlli {{PLURAL:$2|oncān}}: $1.",
-       "ok": "Nopan iti",
+       "ok": "Cualli",
        "retrievedfrom": "Ōquīzqui ītech  \"$1\"",
        "youhavenewmessages": "Tiquimpiya $1 ($2).",
        "youhavenewmessagesmulti": "Tiquimpiya yancuīc tlahcuilōlli īpan $1",
        "editsection": "ticpatlaz",
        "editold": "ticpatlaz",
-       "viewsourceold": "xiquitta tlahtōlcaquiliztilōni",
+       "viewsourceold": "tiquittaz mēyalli",
        "editlink": "ticpatlaz",
-       "viewsourcelink": "tiquittaz tlahtōlcaquiliztilōni",
+       "viewsourcelink": "tiquittaz mēyalli",
        "editsectionhint": "Ticpatlacah: $1",
        "toc": "Inīn tlahcuilōlco",
-       "showtoc": "xiquitta",
+       "showtoc": "ticnēxtīz",
        "hidetoc": "tictlātīz",
        "collapsible-collapse": "Motlàtìs",
        "collapsible-expand": "Monèxtìs",
        "nstab-media": "Mēdiatl",
        "nstab-special": "Nònkuâkìskàtlaìxtlapalli",
        "nstab-project": "Ìtlaìxtlapal in tlayẻkàntekitl",
-       "nstab-image": "Īxiptli",
+       "nstab-image": "Ihcuilōlli",
        "nstab-mediawiki": "Tlahcuilōltzintli",
        "nstab-template": "Nemachiòtl",
        "nstab-help": "Tèpalèwilistli",
        "badtitle": "Ahcualli tōcāitl",
        "badtitletext": "Zāzanilli ticnequi in ītōca cah ahcualli, ahtlein quipiya nozo ahcualtzonhuiliztli interwiki tōcāhuicpa.\nHueliz quimpiya tlahtōl tlein ahmo mohuelītih motequitiltia tōcāpan.",
        "viewsource": "Tiquittaz tlahtōlcaquiliztilōni",
+       "viewsource-title": "Tiquittaz $1 īmēyal",
        "actionthrottled": "Tlachīhualiztli ōmotzacuili",
        "viewsourcetext": "Tihuelīti tiquitta auh ticcopīna inīn zāzanilli ītlahtōlcaquiliztilōni:",
        "namespaceprotected": "Ahmo tiquihuelīti tiquimpatla zāzaniltin īpan '''$1'''.",
        "logout": "Tiquīzaz",
        "userlogout": "Tiquīzaz",
        "notloggedin": "Ahmō ōtimocalac",
-       "nologin": "¿Ahmō ticpiya cuentah? '''$1'''.",
+       "userlogin-noaccount": "Cuix ahmō titlapōhualeh?",
+       "nologin": "Cuix ahmō titlapōhualeh? $1.",
        "nologinlink": "Ticchīhuaz cē cuentah",
        "createaccount": "Ticchīhuaz cuentah",
        "gotaccount": "¿Ye ticpiya cē tlapōhualli? '''$1'''.",
        "powersearch-toggleall": "Mochi",
        "powersearch-togglenone": "Ahtlein",
        "search-external": "Tlatēmotiliztli calāmpa",
-       "preferences": "Tlaēlēhuiliztli",
+       "preferences": "Panitlatlālīlli",
        "mypreferences": "Notlaēlēhuiliz",
        "prefs-edits": "Tlapatlaliztli tlapōhualli:",
        "prefs-skin": "Ēhuatl",
        "right-block": "Tiquintzacuilīz occequīntīn tlatequitiltilīlli",
        "right-blockemail": "Titēquīxtīz tlatequitiltilīlli ic tēch-e-mailīz",
        "right-hideuser": "Ticquīxtīz cē tlatequitiltilīltōcāitl, āuh ichtac",
+       "right-editmyoptions": "Ticpatlaz mopanitlatlālīl",
        "right-import": "Ticcōhuāz zāzaniltin occequīntīn huiquihuīcpa",
        "right-importupload": "Tiquincōhuāz zāzaniltin tlahcuilōlquetzalizhuīcpa",
        "right-patrolmarks": "Tiquinttāz tlapiyalizmachiyōtl īpan yancuīc tlapatlaliztli",
        "sp-contributions-newbies-title": "Yancuīc tlatequitiltilīlli ītlahcuilōl",
        "sp-contributions-blocklog": "Tlatzacuiliztli tlahcuilōlloh",
        "sp-contributions-uploads": "tlahcuilōlquetzaliztli",
-       "sp-contributions-talk": "tēixnāmiquiliztli",
+       "sp-contributions-talk": "zānīlli",
        "sp-contributions-search": "Tiquintlatēmōz tlapatlaliztli",
        "sp-contributions-username": "IP nozo tlatequitiltilīlli ītōcā:",
        "sp-contributions-submit": "Tlatēmōz",
        "import-upload": "Tiquinquetzāz XML tlahcuilōlli",
        "importlogpage": "Tiquincōhuāz tlahcuilōlloh",
        "tooltip-pt-userpage": "Notlatequitiltilīlzāzanil",
-       "tooltip-pt-mytalk": "Notēixnāmiquiliz",
+       "tooltip-pt-mytalk": "Mozānīl",
        "tooltip-pt-preferences": "Mopanitlatlālīl",
        "tooltip-pt-watchlist": "Zāzaniltin tiquintlachiya ic tlapatlaliztli",
        "tooltip-pt-mycontris": "Notlahcuilōl",
index 6c0a4a2..be882ac 100644 (file)
        "recentchangeslinked-summary": "Deze speciale pagina geeft de laatste bewerkingen weer op pagina's waarheen verwezen wordt vanaf een opgegeven pagina of op pagina's in een opgegeven categorie.\nPagina's die op [[Special:Watchlist|uw volglijst]] staan worden '''vet''' weergegeven.",
        "recentchangeslinked-page": "Paginanaam:",
        "recentchangeslinked-to": "Wijzigingen aan pagina's met koppelingen naar deze pagina bekijken",
+       "recentchanges-page-added-to-category": "[[:$1]] aan categorie toegevoegd",
        "upload": "Bestand uploaden",
        "uploadbtn": "Bestand uploaden",
        "reuploaddesc": "Upload annuleren en terugkeren naar het uploadformulier",
        "htmlform-cloner-create": "Meer toevoegen",
        "htmlform-cloner-delete": "Verwijderen",
        "htmlform-cloner-required": "Ten minste één waarde is vereist.",
+       "htmlform-title-not-exists": "[[:$1]] bestaat niet.",
+       "htmlform-user-not-exists": "<strong>$1</strong> bestaat niet.",
+       "htmlform-user-not-valid": "<strong>$1</strong> is geen geldige gebruikersnaam.",
        "sqlite-has-fts": "Versie $1 met ondersteuning voor \"full-text\" zoeken",
        "sqlite-no-fts": "Versie $1 zonder ondersteuning voor \"full-text\" zoeken",
        "logentry-delete-delete": "$1 {{GENDER:$2|heeft}} de pagina $3 verwijderd",
index 3893577..e464d93 100644 (file)
        "foreign-structured-upload-form-label-own-work": "Label for own work toggle",
        "foreign-structured-upload-form-label-infoform-categories": "Label for category selector input\n{{Identical|Category}}",
        "foreign-structured-upload-form-label-infoform-date": "Label for date input\n{{Identical|Date}}",
+       "foreign-structured-upload-form-label-own-work-message-local": "Message shown by local when a user affirms that they are allowed to upload a file to the local wiki.",
+       "foreign-structured-upload-form-label-not-own-work-message-local": "Message shown by local when a user cannot upload a file to the local wiki.",
+       "foreign-structured-upload-form-label-not-own-work-local-local": "Suggests uploading a file via Special:Upload instead of using whatever method they're currently using.",
        "foreign-structured-upload-form-label-own-work-message-default": "Message shown by default when a user affirms that they are allowed to upload a file to a remote wiki.",
        "foreign-structured-upload-form-label-not-own-work-message-default": "Message shown by default when a user cannot upload a file to a remote wiki.",
-       "foreign-structured-upload-form-label-not-own-work-local-default": "Suggests uploading a file locally instead of to a remote wiki. $1 is the name of the local wiki.",
+       "foreign-structured-upload-form-label-not-own-work-local-default": "Suggests uploading a file locally instead of to a remote wiki.",
        "foreign-structured-upload-form-label-own-work-message-wikimediacommons": "Legal message to show when the work is made by the uploader.",
        "foreign-structured-upload-form-label-not-own-work-message-wikimediacommons": "Message to show when the work isn't owned by the uploader.",
        "foreign-structured-upload-form-label-not-own-work-local-wikimediacommons": "Message suggesting the user might want to upload a file locally instead of to Wikimedia Commons. $1 is the name of the local wiki.",
index 9ca6211..0a649c1 100644 (file)
        "tooltip-recreate": "Recreează",
        "tooltip-upload": "Pornește încărcarea",
        "tooltip-rollback": "„Revenire” anulează modificarea(ările) de pe această pagină a(le) ultimului contribuitor printr-o singură apăsare",
-       "tooltip-undo": "„Anulează” șterge această modificare și deschide formularul de modificare în modulul de previzualizare.\nPermite adăugarea unui motiv în descrierea modificărilor.",
+       "tooltip-undo": "„Anulează” revine asupra acestei modificări către versiunea anterioară și deschide formularul de modificare în modul de previzualizare.\nPermite adăugarea unui motiv în descrierea modificărilor.",
        "tooltip-preferences-save": "Salvează preferințele",
        "tooltip-summary": "Descrieți pe scurt modificarea",
        "interlanguage-link-title": "$1 – $2",
index 0afec04..b3214e5 100644 (file)
        "watchlistanontext": "Пожалуйста, войдите, чтобы просмотреть или отредактировать элементы в списке наблюдения.",
        "watchnologin": "Нужно представиться системе",
        "addwatch": "Добавить в список наблюдения",
-       "addedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
+       "addedwatchtext": "Страница «[[:$1]]» вместе с её обсуждением были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
        "addedwatchtext-short": "Страница «$1» была добавлена в ваш список наблюдения.",
        "removewatch": "Удалить из списка наблюдения",
        "removedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были удалены из вашего [[Special:Watchlist|списка наблюдения]].",
        "exbeforeblank": "содержимое до очистки: «$1»",
        "delete-confirm": "$1 — удаление",
        "delete-legend": "Удаление",
-       "historywarning": "<strong>Ð\92нимание:</strong> Ð£ Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b, ÐºÐ¾Ñ\82оÑ\80Ñ\83Ñ\8e Ð²Ñ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c, ÐµÑ\81Ñ\82Ñ\8c Ð¸Ñ\81Ñ\82оÑ\80иÑ\8f Ð¿Ñ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|версий}}:",
+       "historywarning": "<strong>Ð\92нимание:</strong> Ð\92Ñ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ñ\83 ÐºÐ¾Ñ\82оÑ\80ой ÐµÑ\81Ñ\82Ñ\8c Ð¸Ñ\81Ñ\82оÑ\80иÑ\8f Ð¿Ñ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|веÑ\80Ñ\81ии|версий}}:",
        "confirmdeletetext": "Вы запросили полное удаление страницы (или изображения) и всей её истории изменений. Пожалуйста, подтвердите, что вы действительно желаете это сделать, понимаете последствия своих действий, и делаете это в соответствии [[{{MediaWiki:Policy-url}}|с правилами]].",
        "actioncomplete": "Действие выполнено",
        "actionfailed": "Действие не выполнено",
        "undeletepagetext": "{{PLURAL:$1|Следующая $1 страница была удалена|Следующие $1 страниц были удалены|Следующие $1 страницы были удалены|1=Следующая страница была удалена}}, однако {{PLURAL:$1|1=она всё ещё находится в архиве и поэтому может быть восстановлена|они всё ещё находятся в архиве и поэтому могут быть восстановлены}}.\nАрхив может периодически очищаться.",
        "undelete-fieldset-title": "Восстановить версии",
        "undeleteextrahelp": "Для полного восстановления истории страницы оставьте все отметки пустыми и нажмите '''«{{int:undeletebtn}}»'''.\nДля частичного восстановления отметьте те версии страницы, которые нужно восстановить, и нажмите '''«{{int:undeletebtn}}»'''.",
-       "undeleterevisions": "$1 {{PLURAL:$1|версия|версий|версии}} {{PLURAL:$1|удалена|удалены}}",
+       "undeleterevisions": "$1 {{PLURAL:$1|удалённая версия|удалённые версии|удалённых версий}}",
        "undeletehistory": "При восстановлении страницы восстанавливается и её история правок.\nЕсли после удаления была создана новая страница с тем же названием, то восстановленные версии появятся в истории правок перед новыми версиями.",
        "undeleterevdel": "Восстановление не будет произведено, если оно приведёт к частичному удалению последней версии страницы или файла.\nВ подобном случае вы должны снять отметку или показать последние удалённые версии.",
        "undeletehistorynoadmin": "Статья была удалена. Причина удаления и список участников, редактировавших статью до её удаления, показаны ниже. Текст удалённой статьи могут просмотреть только администраторы.",
index a4fb301..16d92be 100644 (file)
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -2,7 +2,6 @@
 <ruleset name="MediaWiki">
        <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
                <!-- Disable failing rules -->
-               <exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
                <exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
                <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.EmptyComment"/>
        </rule>
@@ -22,6 +21,9 @@
        <rule ref="Generic.Files.LineLength">
                <exclude-pattern>*/languages/messages/Messages*.php</exclude-pattern>
        </rule>
+       <rule ref="PSR2.Methods.MethodDeclaration.Underscore">
+               <exclude-pattern>*/includes/StubObject.php</exclude-pattern>
+       </rule>
        <exclude-pattern>node_modules</exclude-pattern>
        <exclude-pattern>vendor</exclude-pattern>
        <exclude-pattern>extensions</exclude-pattern>
index afcd589..5504fd7 100644 (file)
@@ -1850,9 +1850,6 @@ return array(
                'styles' => array(
                        // @todo: Remove mediawiki.page.gallery when cache has cleared
                        'resources/src/mediawiki/page/gallery-print.css' => array( 'media' => 'print' ),
-                       // @todo: Remove mediawiki.action.view.filepage.print.css when cache has cleared
-                       'resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css' =>
-                               array( 'media' => 'print' ),
                        'resources/src/mediawiki.legacy/commonPrint.css' => array( 'media' => 'print' )
                ),
        ),
@@ -1867,9 +1864,6 @@ return array(
                'styles' => array(
                        // @todo: Remove when mediawiki.page.gallery in cached html.
                        'resources/src/mediawiki/page/gallery.css',
-                       // @todo: Remove mediawiki.action.view.filepage.css
-                       // and mediawiki.legacy/images/checker.png when cache has cleared
-                       'resources/src/mediawiki.action/mediawiki.action.view.filepage.css',
                        'resources/src/mediawiki.legacy/shared.css' => array( 'media' => 'screen' )
                ),
        ),
index 3051d52..0807215 100644 (file)
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
                var fieldset,
-                       target = mw.config.get( 'wgRemoteUploadTarget' ),
+                       targets = mw.config.get( 'wgForeignUploadTargets' ),
+                       // Default to using local, but try to use a configured target.
+                       // TODO allow finer configuration of this somehow?
+                       target = ( targets && targets.length ) ? targets[ 0 ] : 'local',
                        $ownWorkMessage = $( '<p>' ).html(
                                mw.message( 'foreign-structured-upload-form-label-own-work-message-' + target ).parse()
                        ),
index 0929661..a367ee0 100644 (file)
@@ -1,4 +1,4 @@
-( function ( mw, OO ) {
+( function ( mw, OO, $ ) {
        /**
         * @class mw.ForeignUpload
         * @extends mw.Upload
         * instead.
         *
         * @constructor
-        * @param {string} [targetHost="commons.wikimedia.org"] Used to set up the target
+        * @param {string} [target="local"] Used to set up the target
         *     wiki. If not remote, this class behaves identically to mw.Upload (unless further subclassed)
+        *     Use the same names as set in $wgForeignFileRepos for this. Also,
+        *     make sure there is an entry in the $wgForeignUploadTargets array
+        *     set to "true" for this name.
         * @param {Object} [apiconfig] Passed to the constructor of mw.ForeignApi or mw.Api, as needed.
         */
-       function ForeignUpload( targetHost, apiconfig ) {
-               var api;
+       function ForeignUpload( target, apiconfig ) {
+               var api, upload = this;
 
-               if ( typeof targetHost === 'object' ) {
-                       // targetHost probably wasn't passed in, it must
+               if ( typeof target === 'object' ) {
+                       // target probably wasn't passed in, it must
                        // be apiconfig
-                       apiconfig = targetHost;
-               } else {
-                       // targetHost is a useful string, set it here
-                       this.targetHost = targetHost || this.targetHost;
+                       apiconfig = target;
+                       target = undefined;
                }
 
-               if ( location.host !== this.targetHost ) {
-                       api = new mw.ForeignApi(
-                               location.protocol + '//' + this.targetHost + '/w/api.php',
-                               apiconfig
-                       );
+               // Resolve defaults etc. - if target isn't passed in, we use
+               // the default.
+               this.target = target || this.target;
+
+               // Now we have several different options.
+               // If the local wiki is the target, then we can skip a bunch of steps
+               // and just return an mw.Api object, because we don't need any special
+               // configuration for that.
+               // However, if the target is a remote wiki, we must check the API
+               // to confirm that the target is one that this site is configured to
+               // support.
+               if ( this.target !== 'local' ) {
+                       api = new mw.Api();
+                       this.apiPromise = api.get( {
+                               action: 'query',
+                               meta: 'filerepoinfo',
+                               friprop: [ 'name', 'scriptDirUrl', 'canUpload' ]
+                       } ).then( function ( data ) {
+                               var i, repo,
+                                       repos = data.query.repos;
+
+                               for ( i in repos ) {
+                                       repo = repos[ i ];
+
+                                       if ( repo.name === upload.target ) {
+                                               // This is our target repo.
+                                               if ( !repo.canUpload ) {
+                                                       // But it's not configured correctly.
+                                                       return $.Deferred().reject( 'repo-cannot-upload' );
+                                               }
+
+                                               return new mw.ForeignApi(
+                                                       repo.scriptDirUrl + '/api.php',
+                                                       apiconfig
+                                               );
+                                       }
+                               }
+                       } );
                } else {
-                       // We'll ignore the CORS and centralauth stuff if we're on Commons already
-                       api = new mw.Api( apiconfig );
+                       // We'll ignore the CORS and centralauth stuff if the target is
+                       // the local wiki.
+                       this.apiPromise = $.Deferred().resolve( new mw.Api( apiconfig ) );
                }
 
-               mw.Upload.call( this, api );
+               // Build the upload object without an API - this class overrides the
+               // actual API call methods to wait for the apiPromise to resolve
+               // before continuing.
+               mw.Upload.call( this, null );
        }
 
        OO.inheritClass( ForeignUpload, mw.Upload );
         * @property targetHost
         * Used to specify the target repository of the upload.
         *
-        * You could override this to point at something that isn't Commons,
-        * but be sure it has the correct templates and is CORS and CentralAuth
-        * ready.
+        * If you set this to something that isn't 'local', you must be sure to
+        * add that target to $wgForeignUploadTargets in LocalSettings, and the
+        * repository must be set up to use CORS and CentralAuth.
+        */
+       ForeignUpload.prototype.target = 'local';
+
+       /**
+        * Override from mw.Upload to make sure the API info is found and allowed
+        */
+       ForeignUpload.prototype.upload = function () {
+               var upload = this;
+               return this.apiPromise.then( function ( api ) {
+                       upload.api = api;
+                       return mw.Upload.prototype.upload.call( upload );
+               } );
+       };
+
+       /**
+        * Override from mw.Upload to make sure the API info is found and allowed
         */
-       ForeignUpload.prototype.targetHost = 'commons.wikimedia.org';
+       ForeignUpload.prototype.uploadToStash = function () {
+               var upload = this;
+               return this.apiPromise.then( function ( api ) {
+                       upload.api = api;
+                       return mw.Upload.prototype.uploadToStash.call( upload );
+               } );
+       };
 
        mw.ForeignUpload = ForeignUpload;
-}( mediaWiki, OO ) );
+}( mediaWiki, OO, jQuery ) );
index 08ba41d..e86559e 100644 (file)
@@ -2,8 +2,9 @@
 
 /**
  * @covers WikiMap
+ *
+ * @group Database
  */
-
 class WikiMapTest extends MediaWikiLangTestCase {
 
        public function setUp() {
@@ -24,23 +25,40 @@ class WikiMapTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( array(
                        'wgConf' => $conf,
                ) );
+
+               TestSites::insertIntoDb();
        }
 
        public function provideGetWiki() {
+               // As provided by $wgConf
                $enwiki = new WikiReference( 'http://en.example.org', '/w/$1' );
                $ruwiki = new WikiReference( '//ru.example.org', '/wiki/$1' );
 
+               // Created from site objects
+               $nlwiki = new WikiReference( 'https://nl.wikipedia.org', '/wiki/$1' );
+               // enwiktionary doesn't have an interwiki id, thus this falls back to minor = lang code
+               $enwiktionary = new WikiReference( 'https://en.wiktionary.org', '/wiki/$1' );
+
                return array(
-                       'unknown' => array( false, 'xyzzy' ),
-                       'enwiki' => array( $enwiki, 'enwiki' ),
-                       'ruwiki' => array( $ruwiki, 'ruwiki' ),
+                       'unknown' => array( null, 'xyzzy' ),
+                       'enwiki (wgConf)' => array( $enwiki, 'enwiki' ),
+                       'ruwiki (wgConf)' => array( $ruwiki, 'ruwiki' ),
+                       'nlwiki (sites)' => array( $nlwiki, 'nlwiki', false ),
+                       'enwiktionary (sites)' => array( $enwiktionary, 'enwiktionary', false ),
+                       'non MediaWiki site' => array( null, 'spam', false ),
                );
        }
 
        /**
         * @dataProvider provideGetWiki
         */
-       public function testGetWiki( $expected, $wikiId ) {
+       public function testGetWiki( $expected, $wikiId, $useWgConf = true ) {
+               if ( !$useWgConf ) {
+                       $this->setMwGlobals( array(
+                               'wgConf' => new SiteConfiguration(),
+                       ) );
+               }
+
                $this->assertEquals( $expected, WikiMap::getWiki( $wikiId ) );
        }
 
@@ -49,6 +67,7 @@ class WikiMapTest extends MediaWikiLangTestCase {
                        'unknown' => array( 'xyzzy', 'xyzzy' ),
                        'enwiki' => array( 'en.example.org', 'enwiki' ),
                        'ruwiki' => array( 'ru.example.org', 'ruwiki' ),
+                       'enwiktionary (sites)' => array( 'en.wiktionary.org', 'enwiktionary' ),
                );
        }
 
@@ -75,6 +94,13 @@ class WikiMapTest extends MediaWikiLangTestCase {
                                'Фу',
                                'вар'
                        ),
+                       'enwiktionary (sites)' => array(
+                               '<a class="external" rel="nofollow" ' .
+                                       'href="https://en.wiktionary.org/wiki/Kitten">Kittens!</a>',
+                               'enwiktionary',
+                               'Kitten',
+                               'Kittens!'
+                       ),
                );
        }
 
@@ -104,6 +130,13 @@ class WikiMapTest extends MediaWikiLangTestCase {
                                'Фу',
                                'вар'
                        ),
+                       'enwiktionary (sites)' => array(
+                               '<a class="external" rel="nofollow" ' .
+                                       'href="https://en.wiktionary.org/wiki/User:Dummy">Whatever</a>',
+                               'enwiktionary',
+                               'Dummy',
+                               'Whatever'
+                       ),
                );
        }
 
@@ -118,6 +151,11 @@ class WikiMapTest extends MediaWikiLangTestCase {
                return array(
                        'unknown' => array( false, 'xyzzy', 'Foo' ),
                        'enwiki' => array( 'http://en.example.org/w/Foo', 'enwiki', 'Foo' ),
+                       'enwiktionary (sites)' => array(
+                               'https://en.wiktionary.org/wiki/Testme',
+                               'enwiktionary',
+                               'Testme'
+                       ),
                        'ruwiki with fragment' => array(
                                '//ru.example.org/wiki/%D0%A4%D1%83#%D0%B2%D0%B0%D1%80',
                                'ruwiki',
index 311ad89..f158fc3 100644 (file)
@@ -131,6 +131,25 @@ class OldChangesListTest extends MediaWikiLangTestCase {
                $this->assertRegExp( '/<li class="[\w\s-]*mw-tag-newbie[\w\s-]*">/', $line );
        }
 
+       public function testRecentChangesLine_numberOfWatchingUsers() {
+               $oldChangesList = $this->getOldChangesList();
+
+               $recentChange = $this->getEditChange();
+               $recentChange->numberofWatchingusers = 100;
+
+               $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+               $this->assertRegExp( "/(number_of_watching_users_RCview: 100)/", $line );
+       }
+
+       public function testRecentChangesLine_watchlistCssClass() {
+               $oldChangesList = $this->getOldChangesList();
+               $oldChangesList->setWatchlistDivs( true );
+
+               $recentChange = $this->getEditChange();
+               $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+               $this->assertRegExp( "/watchlist-0-Cat/", $line );
+       }
+
        private function getNewBotEditChange() {
                $user = $this->getTestUser();
 
diff --git a/tests/phpunit/includes/libs/MemoizedCallableTest.php b/tests/phpunit/includes/libs/MemoizedCallableTest.php
new file mode 100644 (file)
index 0000000..921bba8
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+/**
+ * A MemoizedCallable subclass that stores function return values
+ * in an instance property rather than APC.
+ */
+class ArrayBackedMemoizedCallable extends MemoizedCallable {
+       public $cache = array();
+
+       protected function fetchResult( $key, &$success ) {
+               if ( array_key_exists( $key, $this->cache ) ) {
+                       $success = true;
+                       return $this->cache[$key];
+               }
+               $success = false;
+               return false;
+       }
+
+       protected function storeResult( $key, $result ) {
+               $this->cache[$key] = $result;
+       }
+}
+
+
+/**
+ * PHP Unit tests for MemoizedCallable class.
+ * @covers MemoizedCallable
+ */
+class MemoizedCallableTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * The memoized callable should relate inputs to outputs in the same
+        * way as the original underlying callable.
+        */
+       public function testReturnValuePassedThrough() {
+               $mock = $this->getMock( 'stdClass', array( 'reverse' ) );
+               $mock->expects( $this->any() )
+                       ->method( 'reverse' )
+                       ->will( $this->returnCallback( 'strrev' ) );
+
+               $memoized = new MemoizedCallable( array( $mock, 'reverse' ) );
+               $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
+       }
+
+       /**
+        * Consecutive calls to the memoized callable with the same arguments
+        * should result in just one invocation of the underlying callable.
+        *
+        * @requires function apc_store
+        */
+       public function testCallableMemoized() {
+               $observer = $this->getMock( 'stdClass', array( 'computeSomething' ) );
+               $observer->expects( $this->once() )
+                       ->method( 'computeSomething' )
+                       ->will( $this->returnValue( 'ok' ) );
+
+               $memoized = new ArrayBackedMemoizedCallable( array( $observer, 'computeSomething' ) );
+
+               // First invocation -- delegates to $observer->computeSomething()
+               $this->assertEquals( 'ok', $memoized->invoke() );
+
+               // Second invocation -- returns memoized result
+               $this->assertEquals( 'ok', $memoized->invoke() );
+       }
+
+       /**
+        * @covers MemoizedCallable::invoke
+        */
+       public function testInvokeVariadic() {
+               $memoized = new MemoizedCallable( 'sprintf' );
+               $this->assertEquals(
+                       $memoized->invokeArgs( array( 'this is %s', 'correct' ) ),
+                       $memoized->invoke( 'this is %s', 'correct' )
+               );
+       }
+
+       /**
+        * @covers MemoizedCallable::call
+        */
+       public function testShortcutMethod() {
+               $this->assertEquals(
+                       'this is correct',
+                       MemoizedCallable::call( 'sprintf', array( 'this is %s', 'correct' ) )
+               );
+       }
+
+       /**
+        * Outlier TTL values should be coerced to range 1 - 86400.
+        */
+       public function testTTLMaxMin() {
+               $memoized = new MemoizedCallable( 'abs', 100000 );
+               $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
+
+               $memoized = new MemoizedCallable( 'abs', -10 );
+               $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
+       }
+
+       /**
+        * Closure names should be distinct.
+        */
+       public function testMemoizedClosure() {
+               $a = new MemoizedCallable( function () {
+                       return 'a';
+               } );
+
+               $b = new MemoizedCallable( function () {
+                       return 'b';
+               } );
+
+               $this->assertEquals( $a->invokeArgs(), 'a' );
+               $this->assertEquals( $b->invokeArgs(), 'b' );
+
+               $this->assertNotEquals(
+                       $this->readAttribute( $a, 'callableName' ),
+                       $this->readAttribute( $b, 'callableName' )
+               );
+       }
+
+       /**
+        * @expectedExceptionMessage non-scalar argument
+        * @expectedException        InvalidArgumentException
+        */
+       public function testNonScalarArguments() {
+               $memoized = new MemoizedCallable( 'gettype' );
+               $memoized->invoke( new stdClass() );
+       }
+
+       /**
+        * @expectedExceptionMessage must be an instance of callable
+        * @expectedException        InvalidArgumentException
+        */
+       public function testNotCallable() {
+               $memoized = new MemoizedCallable( 14 );
+       }
+}
index 98b9678..169ae37 100644 (file)
@@ -6,7 +6,7 @@
                var upload = new mw.ForeignUpload();
 
                assert.ok( upload, 'The ForeignUpload constructor is working.' );
-               assert.strictEqual( upload.targetHost, 'commons.wikimedia.org', 'Default target host is correct' );
-               assert.ok( upload.api instanceof mw.ForeignApi, 'API is correctly configured to point at a foreign wiki.' );
+               assert.strictEqual( upload.target, 'local', 'Default target host is correct' );
+               assert.ok( upload.api instanceof mw.Api, 'API is local because default target is local.' );
        } );
 }( mediaWiki ) );