From: jenkins-bot Date: Wed, 7 Oct 2015 16:38:35 +0000 (+0000) Subject: Merge "API: Add ApiQueryAllRevisions" X-Git-Tag: 1.31.0-rc.0~9505 X-Git-Url: http://git.cyclocoop.org//%27%40script%40/%27?a=commitdiff_plain;h=f0b51b42007de9e30f97d2bfbde276053d32c579;hp=2516ca03eda623ea4db358e665970f2c4ac26f3b;p=lhc%2Fweb%2Fwiklou.git Merge "API: Add ApiQueryAllRevisions" --- diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index 5d26ee995a..9aaa32e59e 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -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 ==== diff --git a/autoload.php b/autoload.php index dee87c0bca..a2f432f6fd 100644 --- a/autoload.php +++ b/autoload.php @@ -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', diff --git a/docs/hooks.txt b/docs/hooks.txt index 2d268b8ab3..717dd39007 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -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 diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php index 77c43bf089..d779141052 100644 --- a/includes/CategoryFinder.php +++ b/includes/CategoryFinder.php @@ -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; /** diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index deb85f5b2b..95baa56000 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -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 * diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index ae186bc950..5e7f5b2635 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -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__ ); } } diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 7215cec922..802bfbe948 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -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 ); diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index e29319b594..418ed8b008 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -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" ); + } } /** diff --git a/includes/OutputPage.php b/includes/OutputPage.php index f680d456d2..03ae8c951a 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -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 ); } /** diff --git a/includes/RevisionList.php b/includes/RevisionList.php index 1df0ca0a98..4d72c24a5a 100644 --- a/includes/RevisionList.php +++ b/includes/RevisionList.php @@ -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 ); diff --git a/includes/SiteStats.php b/includes/SiteStats.php index 64e5ea027b..76e7f7eb9f 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -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 ); diff --git a/includes/Title.php b/includes/Title.php index e2cbc8ed20..8e5fae93b9 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -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' ) ); } } diff --git a/includes/User.php b/includes/User.php index 20b75bf095..75649a7881 100644 --- a/includes/User.php +++ b/includes/User.php @@ -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 ); } diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php index 0d1708f314..3a3eb53873 100644 --- a/includes/UserRightsProxy.php +++ b/includes/UserRightsProxy.php @@ -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; diff --git a/includes/WebRequest.php b/includes/WebRequest.php index f402f3b869..bd80c7959b 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -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 index 0000000000..e743d9de16 --- /dev/null +++ b/includes/WebRequestUpload.php @@ -0,0 +1,137 @@ +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; + } +} diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 6215af16a9..325831eef0 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -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 * diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index 4e74ed32e9..78dd5fe926 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -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(); diff --git a/includes/api/ApiQueryFileRepoInfo.php b/includes/api/ApiQueryFileRepoInfo.php index 057b011790..12b9893d93 100644 --- a/includes/api/ApiQueryFileRepoInfo.php +++ b/includes/api/ApiQueryFileRepoInfo.php @@ -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() { diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index 32e0af5cf2..78a1bd4705 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -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 Origin, polo que ten que ser fixado a algo como https://en.wikipedia.org ou https://meta.wikimedia.org. Se este parámetro non coincide coa cabeceira Origin, devolverase unha resposta 403. Se este parámetro coincide coa cabeceira Origin e a orixe está na lista branca, porase unha cabeceira Access-Control-Allow-Origin.", - "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. Pode consultarse unha lista de códigos en [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] con siprop=languages, ou especificando user coa preferencia de lingua do usuario actual, ou especificando content para usar a lingua do contido desta wiki.", + "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] con siprop=languages devolve unha lista de códigos de lingua, ou especificando user coa preferencia de lingua do usuario actual, ou especificando content 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.5 meses ou 2 semanas) ou absoluto (p. ex. 2014-09-18T12:34:56Z). Se se pon kbd>infinite, indefinite, ou never, o bloqueo nunca caducará.", @@ -290,7 +290,7 @@ "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 $1text.", "apihelp-parse-param-effectivelanglinks": "Inclúe ligazóns de idioma proporcionadas polas extensións (para usar con $1prop=langlinks).", - "apihelp-parse-param-section": "Recuperar unicamente o contido deste número de sección ou cando new xera unha nova sección.\n\nA sección new só é atendida cando se especifica text.", + "apihelp-parse-param-section": "Analizar unicamente o contido deste número de sección.\n\nCando nova, analiza $1text e $1sectiontitle como se fose a engadir unha nova sección da páxina.\n\nnovo só se permite cando especifica text.", "apihelp-parse-param-sectiontitle": "Novo título de sección cando section é new.\n\nA diferenza da edición de páxinas, non se oculta no summary 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 $1disablelimitreport no seu lugar.", @@ -823,15 +823,15 @@ "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 ([[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]] 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 Main Page e MediaWiki", "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 ([[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]] 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 {{DISPLAYTITLE:}}.", @@ -1168,6 +1168,9 @@ "apihelp-setnotificationtimestamp-example-page": "Restaurar o estado de notificación para a Páxina Principal.", "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixar o selo de tempo de notificación para a Main page 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 {{ns:user}}.", + "apihelp-stashedit-param-title": "Título da páxina que se está a editar.", + "apihelp-stashedit-param-section": "Número de selección. O 0 é para a sección superior, novo 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.", diff --git a/includes/api/i18n/ksh.json b/includes/api/i18n/ksh.json index d5b33ca3cf..6ae334b4d7 100644 --- a/includes/api/i18n/ksh.json +++ b/includes/api/i18n/ksh.json @@ -660,6 +660,7 @@ "apihelp-query+pageprops-example-simple": "Holl de Eijeschaffte för di Sigge „Main Page“ un „MediaWiki“.", "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 __NOTOC__ dren vörkütt.", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index 8e9120bdda..b340d5e4d1 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -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}}:", diff --git a/includes/api/i18n/ps.json b/includes/api/i18n/ps.json index dd7a21caec..76d94e2e69 100644 --- a/includes/api/i18n/ps.json +++ b/includes/api/i18n/ps.json @@ -28,7 +28,7 @@ "apihelp-login-param-domain": "شپول (اختياري).", "apihelp-login-example-login": "ننوتل.", "apihelp-move-description": "يو مخ لېږدول.", - "apihelp-query+search-example-simple": "د مانا پلټل.", + "apihelp-query+search-example-simple": "د meaning پلټل.", "apihelp-query+search-example-text": "د مانا لپاره متنونه پلټل.", "apihelp-query+watchlist-paramvalue-prop-title": "د يو مخ سرليک ورگډوي.", "apihelp-tag-param-reason": "د بدلون سبب.", diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index e1b9b27a4b..5531245679 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -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', diff --git a/includes/db/Database.php b/includes/db/Database.php index f044002d3f..b9d344f685 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -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 ); diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 928de61612..6453854a7b 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -451,3 +451,9 @@ This may indicate a bug in the software.', */ class DBUnexpectedError extends DBError { } + +/** + * @ingroup Database + */ +class DBReadOnlyError extends DBError { +} diff --git a/includes/db/loadbalancer/LBFactory.php b/includes/db/loadbalancer/LBFactory.php index e5fb09435f..a06d826e2c 100644 --- a/includes/db/loadbalancer/LBFactory.php +++ b/includes/db/loadbalancer/LBFactory.php @@ -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; } diff --git a/includes/db/loadbalancer/LoadBalancer.php b/includes/db/loadbalancer/LoadBalancer.php index a0ef753b42..3350d19ee5 100644 --- a/includes/db/loadbalancer/LoadBalancer.php +++ b/includes/db/loadbalancer/LoadBalancer.php @@ -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 diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php index cc70960fe3..83d5f4ca53 100644 --- a/includes/externalstore/ExternalStoreDB.php +++ b/includes/externalstore/ExternalStoreDB.php @@ -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 ) { diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 59b2fd60c0..b6ddbad256 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -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, diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php index 4f64f0222f..0ade6164f2 100644 --- a/includes/filebackend/filejournal/DBFileJournal.php +++ b/includes/filebackend/filejournal/DBFileJournal.php @@ -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() { diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php index b81cf3e497..9d4f0090d4 100644 --- a/includes/filebackend/lockmanager/DBLockManager.php +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -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;" ); } diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index dfdb37537c..f3a560b4db 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -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(); diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index f49b716fec..357f0b92f8 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -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 ); diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 389f081c19..02d859f28c 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -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( diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 43d1eb5f6b..588ae6b2fd 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -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' ) ); } } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index d5179cbc50..390b7fe5e0 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -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; } diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php index fd92e11a06..42ee9e46a7 100644 --- a/includes/filerepo/file/OldLocalFile.php +++ b/includes/filerepo/file/OldLocalFile.php @@ -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; } diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php index 89aeaae68a..7a49f9be89 100644 --- a/includes/interwiki/Interwiki.php +++ b/includes/interwiki/Interwiki.php @@ -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', diff --git a/includes/jobqueue/JobRunner.php b/includes/jobqueue/JobRunner.php index 130436293a..7ce731df38 100644 --- a/includes/jobqueue/JobRunner.php +++ b/includes/jobqueue/JobRunner.php @@ -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 ); } diff --git a/includes/jobqueue/jobs/AssembleUploadChunksJob.php b/includes/jobqueue/jobs/AssembleUploadChunksJob.php index a1de77e63b..4de19bc5ae 100644 --- a/includes/jobqueue/jobs/AssembleUploadChunksJob.php +++ b/includes/jobqueue/jobs/AssembleUploadChunksJob.php @@ -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 ); diff --git a/includes/jobqueue/jobs/PublishStashedFileJob.php b/includes/jobqueue/jobs/PublishStashedFileJob.php index 8a180ec35c..59166e8035 100644 --- a/includes/jobqueue/jobs/PublishStashedFileJob.php +++ b/includes/jobqueue/jobs/PublishStashedFileJob.php @@ -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 index 0000000000..14de6b9034 --- /dev/null +++ b/includes/libs/MemoizedCallable.php @@ -0,0 +1,151 @@ +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 ); + } +} diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 49966cf7bc..c6fa9148e7 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -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 diff --git a/includes/libs/ObjectFactory.php b/includes/libs/ObjectFactory.php index 1cb544b8b8..0b9aa7ca60 100644 --- a/includes/libs/ObjectFactory.php +++ b/includes/libs/ObjectFactory.php @@ -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. * diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0dbbaba987..522c5d7196 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -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; } diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index 647d938919..ecdf48fc95 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -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 { diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php index 55e84b05f2..bef04569b1 100644 --- a/includes/libs/objectcache/EmptyBagOStuff.php +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -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; } diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index b685e41fc3..d4044e9f16 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -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]; } diff --git a/includes/libs/objectcache/ReplicatedBagOStuff.php b/includes/libs/objectcache/ReplicatedBagOStuff.php index 9e80e9fd1a..98120477d3 100644 --- a/includes/libs/objectcache/ReplicatedBagOStuff.php +++ b/includes/libs/objectcache/ReplicatedBagOStuff.php @@ -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 ) { diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index b1d3ec2d60..4beb627d6a 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -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(); diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index c480aa08d9..592565ff83 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -28,7 +28,13 @@ * @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; diff --git a/includes/libs/objectcache/XCacheBagOStuff.php b/includes/libs/objectcache/XCacheBagOStuff.php index 9dbff6f174..dc34f557a3 100644 --- a/includes/libs/objectcache/XCacheBagOStuff.php +++ b/includes/libs/objectcache/XCacheBagOStuff.php @@ -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 ) ) { diff --git a/includes/mail/UserMailer.php b/includes/mail/UserMailer.php index 3c28c5f5d0..49ce21c7d2 100644 --- a/includes/mail/UserMailer.php +++ b/includes/mail/UserMailer.php @@ -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 diff --git a/includes/objectcache/MemcachedBagOStuff.php b/includes/objectcache/MemcachedBagOStuff.php index 7d1274921c..412f017302 100644 --- a/includes/objectcache/MemcachedBagOStuff.php +++ b/includes/objectcache/MemcachedBagOStuff.php @@ -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 ); } diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php index 1b2c8db62e..a7b48a2a2e 100644 --- a/includes/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/objectcache/MemcachedPeclBagOStuff.php @@ -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 ); diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php index b69077270e..c05ecb6d79 100644 --- a/includes/objectcache/MultiWriteBagOStuff.php +++ b/includes/objectcache/MultiWriteBagOStuff.php @@ -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; } diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php index 7d9903fe78..e6b3f9eb2f 100644 --- a/includes/objectcache/RedisBagOStuff.php +++ b/includes/objectcache/RedisBagOStuff.php @@ -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; diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index 91189c8145..c2e5bd760c 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -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 ) { diff --git a/includes/page/WikiFilePage.php b/includes/page/WikiFilePage.php index bfcd4c3111..c508abe389 100644 --- a/includes/page/WikiFilePage.php +++ b/includes/page/WikiFilePage.php @@ -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 { diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 98cc80abd0..e47e06cb49 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -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 diff --git a/includes/pager/IndexPager.php b/includes/pager/IndexPager.php index 7a5952f167..f0e7f3e809 100644 --- a/includes/pager/IndexPager.php +++ b/includes/pager/IndexPager.php @@ -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; diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index a637b935e6..0e5354790c 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -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 ) ); } /** diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 1857d23ad0..eabafbd540 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -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 ) ); diff --git a/includes/site/DBSiteStore.php b/includes/site/DBSiteStore.php index 1193bd65f6..17764a1985 100644 --- a/includes/site/DBSiteStore.php +++ b/includes/site/DBSiteStore.php @@ -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_' - ); - } - } diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php index e3230fff84..e61179b02e 100644 --- a/includes/site/SiteSQLStore.php +++ b/includes/site/SiteSQLStore.php @@ -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 ); } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 91e84e4dec..fc7eeb17d5 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -396,7 +396,7 @@ class SpecialSearch extends SpecialPage { $out->addHtml( "" ); - Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out ) ); + Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out, $term ) ); } diff --git a/includes/utils/BatchRowIterator.php b/includes/utils/BatchRowIterator.php index 59350e6fe9..07cb2bcd0e 100644 --- a/includes/utils/BatchRowIterator.php +++ b/includes/utils/BatchRowIterator.php @@ -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 diff --git a/includes/utils/BatchRowWriter.php b/includes/utils/BatchRowWriter.php index 377ed852d5..13cab5bd2b 100644 --- a/includes/utils/BatchRowWriter.php +++ b/includes/utils/BatchRowWriter.php @@ -22,7 +22,7 @@ */ class BatchRowWriter { /** - * @var DatabaseBase $db The database to write to + * @var IDatabase $db The database to write to */ protected $db; diff --git a/includes/utils/FileContentsHasher.php b/includes/utils/FileContentsHasher.php index 655c1d0b07..c86691903b 100644 --- a/includes/utils/FileContentsHasher.php +++ b/includes/utils/FileContentsHasher.php @@ -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; } diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index f3bd212268..da9c10f1aa 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -536,6 +536,7 @@ "changeemail-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.", "changeemail-oldemail": "Цяперашні адрас электроннай пошты:", "changeemail-newemail": "Новы адрас электроннай пошты:", + "changeemail-newemail-help": "Поле трэба пакінуць пустым, калі вы хочаце выдаліць свой адрас электроннай пошты. Пасьля выдаленьня вы ня зможаце ануляваць забыты пароль і ня будзеце атрымліваць лісты электроннай пошты з гэтай вікі.", "changeemail-none": "(няма)", "changeemail-password": "Ваш пароль у {{GRAMMAR:месны|{{SITENAME}}}}:", "changeemail-submit": "Зьмяніць адрас электроннай пошты", @@ -1637,7 +1638,7 @@ "nopagetext": "Пазначанай мэтавай старонкі не існуе.", "pager-newer-n": "$1 {{PLURAL:$1|навейшая|навейшыя|навейшых}}", "pager-older-n": "$1 {{PLURAL:$1|старэйшая|старэйшыя|старэйшых}}", - "suppress": "Рэвізаваць", + "suppress": "Падавіць вэрсію", "querypage-disabled": "Гэта спэцыяльная старонка адключаная для падвышэньня прадукцыйнасьці", "apihelp": "Даведка API", "apihelp-no-such-module": "Модуль «$1» ня знойдзены.", diff --git a/languages/i18n/cu.json b/languages/i18n/cu.json index 241494e385..8e935b721e 100644 --- a/languages/i18n/cu.json +++ b/languages/i18n/cu.json @@ -175,6 +175,7 @@ "nstab-template": "обраꙁьць", "nstab-help": "страница помощи", "nstab-category": "катигорїꙗ", + "mainpage-nstab": "главьна страница", "nosuchspecialpage": "си нарочнꙑ страницѧ нѣстъ", "error": "блаꙁна", "internalerror": "вънѫтрѣнꙗ блаꙁна", @@ -577,7 +578,6 @@ "block-log-flags-anononly": "тъкъмо анѡнѷмьнꙑ польꙃєватєлє", "move-page": "прѣимєнованиѥ ⁖ $1 ⁖", "move-page-legend": "страницѧ прѣимєнованиѥ", - "movearticle": "страница :", "newtitle": "ново имѧ :", "move-watch": "си страницѧ блюдєниѥ", "movepagebtn": "прѣимєнованиѥ", diff --git a/languages/i18n/diq.json b/languages/i18n/diq.json index 2c2770d0e1..322426f7a9 100644 --- a/languages/i18n/diq.json +++ b/languages/i18n/diq.json @@ -304,6 +304,7 @@ "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", @@ -2147,7 +2148,7 @@ "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", @@ -3096,7 +3097,7 @@ "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.", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 2005ee81a2..29013931c8 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1425,6 +1425,9 @@ "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.", diff --git a/languages/i18n/fr.json b/languages/i18n/fr.json index 0a1f8eea9e..f45db4986a 100644 --- a/languages/i18n/fr.json +++ b/languages/i18n/fr.json @@ -261,7 +261,7 @@ "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.", diff --git a/languages/i18n/gl.json b/languages/i18n/gl.json index 83e65f851b..2418fdec04 100644 --- a/languages/i18n/gl.json +++ b/languages/i18n/gl.json @@ -359,7 +359,7 @@ "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 das súas edicións nesta páxina.", @@ -436,7 +436,7 @@ "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}}", @@ -534,11 +534,11 @@ "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:", @@ -666,6 +666,7 @@ "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.", @@ -822,7 +823,7 @@ "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\".", @@ -923,7 +924,7 @@ "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", @@ -1036,20 +1037,20 @@ "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)", @@ -1236,6 +1237,7 @@ "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", @@ -1329,6 +1331,7 @@ "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": " #
\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 #
", "upload-success-subj": "A carga realizouse correctamente", @@ -1359,6 +1362,9 @@ "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.", @@ -1646,7 +1652,7 @@ "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\".", @@ -1763,7 +1769,7 @@ "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", @@ -1814,7 +1820,7 @@ "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", @@ -2132,7 +2138,7 @@ "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.", @@ -2142,7 +2148,7 @@ "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", @@ -3196,6 +3202,11 @@ "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", diff --git a/languages/i18n/jv.json b/languages/i18n/jv.json index 1c8a949f09..6cbf7eb524 100644 --- a/languages/i18n/jv.json +++ b/languages/i18n/jv.json @@ -2135,7 +2135,7 @@ "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", diff --git a/languages/i18n/ksh.json b/languages/i18n/ksh.json index 3c575b55d0..f01ea6a5d4 100644 --- a/languages/i18n/ksh.json +++ b/languages/i18n/ksh.json @@ -707,7 +707,7 @@ "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''", @@ -1230,6 +1230,7 @@ "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.", @@ -2134,7 +2135,7 @@ "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", diff --git a/languages/i18n/nah.json b/languages/i18n/nah.json index f00d523cae..85ae81f54b 100644 --- a/languages/i18n/nah.json +++ b/languages/i18n/nah.json @@ -131,34 +131,35 @@ "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": " ī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", @@ -170,11 +171,11 @@ "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", @@ -220,18 +221,18 @@ "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", @@ -251,7 +252,7 @@ "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", @@ -277,6 +278,7 @@ "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'''.", @@ -297,7 +299,8 @@ "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'''.", @@ -485,7 +488,7 @@ "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", @@ -569,6 +572,7 @@ "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", @@ -867,7 +871,7 @@ "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", @@ -955,7 +959,7 @@ "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", diff --git a/languages/i18n/nl.json b/languages/i18n/nl.json index 6c0a4a2a22..be882ac416 100644 --- a/languages/i18n/nl.json +++ b/languages/i18n/nl.json @@ -1280,6 +1280,7 @@ "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", @@ -3211,6 +3212,9 @@ "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": "$1 bestaat niet.", + "htmlform-user-not-valid": "$1 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", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 3893577dc2..e464d93169 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1598,9 +1598,12 @@ "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.", diff --git a/languages/i18n/ro.json b/languages/i18n/ro.json index 9ca6211263..0a649c1bf2 100644 --- a/languages/i18n/ro.json +++ b/languages/i18n/ro.json @@ -2359,7 +2359,7 @@ "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", diff --git a/languages/i18n/ru.json b/languages/i18n/ru.json index 0afec04a55..b3214e53f3 100644 --- a/languages/i18n/ru.json +++ b/languages/i18n/ru.json @@ -1848,7 +1848,7 @@ "watchlistanontext": "Пожалуйста, войдите, чтобы просмотреть или отредактировать элементы в списке наблюдения.", "watchnologin": "Нужно представиться системе", "addwatch": "Добавить в список наблюдения", - "addedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были добавлены в ваш [[Special:Watchlist|список наблюдения]].", + "addedwatchtext": "Страница «[[:$1]]» вместе с её обсуждением были добавлены в ваш [[Special:Watchlist|список наблюдения]].", "addedwatchtext-short": "Страница «$1» была добавлена в ваш список наблюдения.", "removewatch": "Удалить из списка наблюдения", "removedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были удалены из вашего [[Special:Watchlist|списка наблюдения]].", @@ -1893,7 +1893,7 @@ "exbeforeblank": "содержимое до очистки: «$1»", "delete-confirm": "$1 — удаление", "delete-legend": "Удаление", - "historywarning": "Внимание: У страницы, которую вы собираетесь удалить, есть история правок, содержащая $1 {{PLURAL:$1|версию|версий}}:", + "historywarning": "Внимание: Вы собираетесь удалить страницу, у которой есть история правок, содержащая $1 {{PLURAL:$1|версию|версии|версий}}:", "confirmdeletetext": "Вы запросили полное удаление страницы (или изображения) и всей её истории изменений. Пожалуйста, подтвердите, что вы действительно желаете это сделать, понимаете последствия своих действий, и делаете это в соответствии [[{{MediaWiki:Policy-url}}|с правилами]].", "actioncomplete": "Действие выполнено", "actionfailed": "Действие не выполнено", @@ -2000,7 +2000,7 @@ "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": "Статья была удалена. Причина удаления и список участников, редактировавших статью до её удаления, показаны ниже. Текст удалённой статьи могут просмотреть только администраторы.", diff --git a/phpcs.xml b/phpcs.xml index a4fb301000..16d92be79a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -2,7 +2,6 @@ - @@ -22,6 +21,9 @@ */languages/messages/Messages*.php + + */includes/StubObject.php + node_modules vendor extensions diff --git a/resources/Resources.php b/resources/Resources.php index afcd5898b2..5504fd7857 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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' ) ), ), diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js index 3051d52e73..0807215583 100644 --- a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js +++ b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js @@ -53,7 +53,10 @@ */ 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 = $( '

' ).html( mw.message( 'foreign-structured-upload-form-label-own-work-message-' + target ).parse() ), diff --git a/resources/src/mediawiki/mediawiki.ForeignUpload.js b/resources/src/mediawiki/mediawiki.ForeignUpload.js index 0929661648..a367ee05d5 100644 --- a/resources/src/mediawiki/mediawiki.ForeignUpload.js +++ b/resources/src/mediawiki/mediawiki.ForeignUpload.js @@ -1,4 +1,4 @@ -( function ( mw, OO ) { +( function ( mw, OO, $ ) { /** * @class mw.ForeignUpload * @extends mw.Upload @@ -13,33 +13,71 @@ * 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 ); @@ -48,11 +86,33 @@ * @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 ) ); diff --git a/tests/phpunit/includes/WikiMapTest.php b/tests/phpunit/includes/WikiMapTest.php index 08ba41d9a1..e86559e7d8 100644 --- a/tests/phpunit/includes/WikiMapTest.php +++ b/tests/phpunit/includes/WikiMapTest.php @@ -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( + 'Kittens!', + 'enwiktionary', + 'Kitten', + 'Kittens!' + ), ); } @@ -104,6 +130,13 @@ class WikiMapTest extends MediaWikiLangTestCase { 'Фу', 'вар' ), + 'enwiktionary (sites)' => array( + 'Whatever', + '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', diff --git a/tests/phpunit/includes/changes/OldChangesListTest.php b/tests/phpunit/includes/changes/OldChangesListTest.php index 311ad89c2a..f158fc3edf 100644 --- a/tests/phpunit/includes/changes/OldChangesListTest.php +++ b/tests/phpunit/includes/changes/OldChangesListTest.php @@ -131,6 +131,25 @@ class OldChangesListTest extends MediaWikiLangTestCase { $this->assertRegExp( '/

  • /', $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 index 0000000000..921bba8466 --- /dev/null +++ b/tests/phpunit/includes/libs/MemoizedCallableTest.php @@ -0,0 +1,134 @@ +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 ); + } +} diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.ForeignUpload.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.ForeignUpload.test.js index 98b9678142..169ae37194 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.ForeignUpload.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.ForeignUpload.test.js @@ -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 ) );