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 ====
'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',
+ 'Database' => __DIR__ . '/includes/db/Database.php',
'DatabaseBase' => __DIR__ . '/includes/db/Database.php',
'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php',
'DatabaseLag' => __DIR__ . '/maintenance/lag.php',
'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',
'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',
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
$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
/** @var string "AND" or "OR" */
protected $mode;
- /** @var DatabaseBase Read-DB slave */
+ /** @var IDatabase Read-DB slave */
protected $dbr;
/**
$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.
$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
*
$logEntry->setComment( $logComment );
$logid = $logEntry->insert();
$logEntry->publish( $logid );
+
+ $status->value = $logid;
}
} else {
$status = Status::newFatal( 'cannotdelete',
$status = $file->delete( $reason, $suppress, $user );
if ( $status->isOK() ) {
$dbw->commit( __METHOD__ );
+ $status->value = $deleteStatus->value; // log id
} else {
$dbw->rollback( __METHOD__ );
}
}
/**
- * 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
*
* @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 );
$mValidNamespaces[] = $ns;
}
}
+ // T109137: sort numerically
+ sort( $mValidNamespaces, SORT_NUMERIC );
}
return $mValidNamespaces;
*/
public function run() {
try {
- $this->checkMaxLag();
try {
$this->main();
} catch ( ErrorPageError $e ) {
$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" );
+ }
}
/**
}
}
- /**
- * Checks if the request should abort due to a lagged server,
- * for given maxlag parameter.
- * @return bool
- */
- private function checkMaxLag() {
- $maxLag = $this->context->getRequest()->getVal( 'maxlag' );
- if ( !is_null( $maxLag ) ) {
- list( $host, $lag ) = wfGetLB()->getMaxLag();
- if ( $lag > $maxLag ) {
- $resp = $this->context->getRequest()->response();
- $resp->statusHeader( 503 );
- $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
- $resp->header( 'X-Database-Lag: ' . intval( $lag ) );
- $resp->header( 'Content-Type: text/plain' );
- if ( $this->config->get( 'ShowHostnames' ) ) {
- echo "Waiting for $host: $lag seconds lagged\n";
- } else {
- echo "Waiting for a database server: $lag seconds lagged\n";
- }
-
- exit;
- }
- }
- return true;
- }
-
private function main() {
global $wgTitle, $wgTrxProfilerLimits;
/** @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
* @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 );
}
/**
/**
* 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 );
/**
* 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 );
* 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 );
*
* @note: this handles RawMessage poorly
*
- * @param string $type
+ * @param string|bool $type
* @return array
*/
protected function getStatusArray( $type = false ) {
/**
* Updates page_touched for this page; called from LinksUpdate.php
*
- * @param integer $purgeTime TS_MW timestamp [optional]
+ * @param string $purgeTime [optional] TS_MW timestamp
* @return bool True if the update succeeded
*/
public function invalidateCache( $purgeTime = null ) {
* 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' ) );
}
}
$data['mVersion'] = self::VERSION;
$key = wfMemcKey( 'user', 'id', $this->mId );
- $opts = array( 'since' => wfGetDB( DB_SLAVE )->trxTimestamp() );
+ $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
ObjectCache::getMainWANInstance()->set( $key, $data, 3600, $opts );
}
*
* @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;
}
}
-/**
- * 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.
*
--- /dev/null
+<?php
+/**
+ * Object to access the $_FILES array
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Object to access the $_FILES array
+ *
+ * @ingroup HTTP
+ */
+class WebRequestUpload {
+ protected $request;
+ protected $doesExist;
+ protected $fileInfo;
+
+ /**
+ * Constructor. Should only be called by WebRequest
+ *
+ * @param WebRequest $request The associated request
+ * @param string $key Key in $_FILES array (name of form field)
+ */
+ public function __construct( $request, $key ) {
+ $this->request = $request;
+ $this->doesExist = isset( $_FILES[$key] );
+ if ( $this->doesExist ) {
+ $this->fileInfo = $_FILES[$key];
+ }
+ }
+
+ /**
+ * Return whether a file with this name was uploaded.
+ *
+ * @return bool
+ */
+ public function exists() {
+ return $this->doesExist;
+ }
+
+ /**
+ * Return the original filename of the uploaded file
+ *
+ * @return string|null Filename or null if non-existent
+ */
+ public function getName() {
+ if ( !$this->exists() ) {
+ return null;
+ }
+
+ global $wgContLang;
+ $name = $this->fileInfo['name'];
+
+ # Safari sends filenames in HTML-encoded Unicode form D...
+ # Horrid and evil! Let's try to make some kind of sense of it.
+ $name = Sanitizer::decodeCharReferences( $name );
+ $name = $wgContLang->normalize( $name );
+ wfDebug( __METHOD__ . ": {$this->fileInfo['name']} normalized to '$name'\n" );
+ return $name;
+ }
+
+ /**
+ * Return the file size of the uploaded file
+ *
+ * @return int File size or zero if non-existent
+ */
+ public function getSize() {
+ if ( !$this->exists() ) {
+ return 0;
+ }
+
+ return $this->fileInfo['size'];
+ }
+
+ /**
+ * Return the path to the temporary file
+ *
+ * @return string|null Path or null if non-existent
+ */
+ public function getTempName() {
+ if ( !$this->exists() ) {
+ return null;
+ }
+
+ return $this->fileInfo['tmp_name'];
+ }
+
+ /**
+ * Return the upload error. See link for explanation
+ * http://www.php.net/manual/en/features.file-upload.errors.php
+ *
+ * @return int One of the UPLOAD_ constants, 0 if non-existent
+ */
+ public function getError() {
+ if ( !$this->exists() ) {
+ return 0; # UPLOAD_ERR_OK
+ }
+
+ return $this->fileInfo['error'];
+ }
+
+ /**
+ * Returns whether this upload failed because of overflow of a maximum set
+ * in php.ini
+ *
+ * @return bool
+ */
+ public function isIniSizeOverflow() {
+ if ( $this->getError() == UPLOAD_ERR_INI_SIZE ) {
+ # PHP indicated that upload_max_filesize is exceeded
+ return true;
+ }
+
+ $contentLength = $this->request->getHeader( 'CONTENT_LENGTH' );
+ if ( $contentLength > wfShorthandToInteger( ini_get( 'post_max_size' ) ) ) {
+ # post_max_size is exceeded
+ return true;
+ }
+
+ return false;
+ }
+}
$content .= $this->msg( 'pageinfo-footer' )->parse();
}
- // Page credits
- /*if ( $this->page->exists() ) {
- $content .= Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $this->getContributors() );
- }*/
-
return $content;
}
* @return string The table with the row added
*/
protected function addRow( $table, $name, $value, $id ) {
- return $table . Html::rawElement( 'tr', $id === null ? array() : array( 'id' => 'mw-' . $id ),
- Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) .
- Html::rawElement( 'td', array(), $value )
- );
+ return $table .
+ Html::rawElement(
+ 'tr',
+ $id === null ? array() : array( 'id' => 'mw-' . $id ),
+ Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) .
+ Html::rawElement( 'td', array(), $value )
+ );
}
/**
$policy = $this->page->getRobotPolicy( 'view', $pOutput );
$pageInfo['header-basic'][] = array(
// Messages: pageinfo-robot-index, pageinfo-robot-noindex
- $this->msg( 'pageinfo-robot-policy' ), $this->msg( "pageinfo-robot-${policy['index']}" )
+ $this->msg( 'pageinfo-robot-policy' ),
+ $this->msg( "pageinfo-robot-${policy['index']}" )
);
$unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
// Subpages of this page, if subpages are enabled for the current NS
if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
- $prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' );
+ $prefixIndex = SpecialPage::getTitleFor(
+ 'Prefixindex', $title->getPrefixedText() . '/' );
$pageInfo['header-basic'][] = array(
Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
$this->msg( 'pageinfo-subpages-value' )
$sources = $title->getCascadeProtectionSources(); // Array deferencing is in PHP 5.4 :(
foreach ( $sources[0] as $sourceTitle ) {
- $cascadingFrom .= Html::rawElement( 'li', array(), Linker::linkKnown( $sourceTitle ) );
+ $cascadingFrom .= Html::rawElement(
+ 'li', array(), Linker::linkKnown( $sourceTitle ) );
}
$cascadingFrom = Html::rawElement( 'ul', array(), $cascadingFrom );
$this->msg( 'pageinfo-lasttime' ),
Linker::linkKnown(
$title,
- htmlspecialchars( $lang->userTimeAndDate( $this->page->getTimestamp(), $user ) ),
+ htmlspecialchars(
+ $lang->userTimeAndDate( $this->page->getTimestamp(), $user )
+ ),
array(),
array( 'oldid' => $this->page->getLatest() )
)
// Recent number of edits (within past 30 days)
$pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
+ $this->msg( 'pageinfo-recent-edits',
+ $lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
$lang->formatNum( $pageCounts['recent_edits'] )
);
// Recent number of distinct authors
$pageInfo['header-edits'][] = array(
- $this->msg( 'pageinfo-recent-authors' ), $lang->formatNum( $pageCounts['recent_authors'] )
+ $this->msg( 'pageinfo-recent-authors' ),
+ $lang->formatNum( $pageCounts['recent_authors'] )
);
// Array of MagicWord objects
$title = $page->getTitle();
$id = $title->getArticleID();
+ $dbr = wfGetDB( DB_SLAVE );
$dbrWatchlist = wfGetDB( DB_SLAVE, 'watchlist' );
+
+ $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
+
$result = array();
// Number of page watchers
array(
'wl_namespace' => $title->getNamespace(),
'wl_title' => $title->getDBkey(),
- 'wl_notificationtimestamp >= ' . $dbrWatchlist->addQuotes( $threshold ) .
- ' OR wl_notificationtimestamp IS NULL'
+ 'wl_notificationtimestamp >= ' .
+ $dbrWatchlist->addQuotes( $threshold ) .
+ ' OR wl_notificationtimestamp IS NULL'
),
$fname
);
$result['visitingWatchers'] = $visitingWatchers;
}
- $dbr = wfGetDB( DB_SLAVE );
// Total number of edits
$edits = (int)$dbr->selectField(
'revision',
$fname
);
- $setOpts = array( 'since' => $dbr->trxTimestamp() );
-
return $result;
},
86400 * 7
# "ThisSite user(s) A, B and C"
if ( count( $user_names ) ) {
- $user = $this->msg( 'siteusers' )->rawParams( $lang->listToText( $user_names ) )->params(
- count( $user_names ) )->escaped();
+ $user = $this->msg( 'siteusers' )
+ ->rawParams( $lang->listToText( $user_names ) )
+ ->params( count( $user_names ) )->escaped();
} else {
$user = false;
}
if ( count( $anon_ips ) ) {
- $anon = $this->msg( 'anonusers' )->rawParams( $lang->listToText( $anon_ips ) )->params(
- count( $anon_ips ) )->escaped();
+ $anon = $this->msg( 'anonusers' )
+ ->rawParams( $lang->listToText( $anon_ips ) )
+ ->params( count( $anon_ips ) )->escaped();
} else {
$anon = false;
}
}
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' );
$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() {
"apihelp-main-param-servedby": "Inclúa o nome do servidor que servía a solicitude nos resultados.",
"apihelp-main-param-curtimestamp": "Incluir a marca de tempo actual no resultado.",
"apihelp-main-param-origin": "Cando se accede á API usando unha petición AJAX entre-dominios (CORS), inicializar o parámetro co dominio orixe. Isto debe incluírse en calquera petición pre-flight, e polo tanto debe ser parte da petición URI (non do corpo POST). Debe coincidir exactamente cunha das orixes na cabeceira <code>Origin</code>, polo que ten que ser fixado a algo como <kbd>https://en.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parámetro non coincide coa cabeceira <code>Origin</code>, devolverase unha resposta 403. Se este parámetro coincide coa cabeceira <code>Origin</code> e a orixe está na lista branca, porase unha cabeceira <code>Access-Control-Allow-Origin</code>.",
- "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. Pode consultarse unha lista de códigos en <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd>, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
+ "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd> devolve unha lista de códigos de lingua, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
"apihelp-block-description": "Bloquear un usuario.",
"apihelp-block-param-user": "Nome de usuario, dirección ou rango de IPs que quere bloquear.",
"apihelp-block-param-expiry": "Tempo de caducidade. Pode ser relativo (p. ex.<kbd>5 meses</kbd> ou <kbd>2 semanas</kbd>) ou absoluto (p. ex. 2014-09-18T12:34:56Z</kbd>). Se se pon kbd>infinite</kbd>, <kbd>indefinite</kbd>, ou <kbd>never</kbd>, o bloqueo nunca caducará.",
"apihelp-parse-param-pst": "Fai unha transformación antes de gardar a entrada antes de analizala. Válida unicamente para usar con texto.",
"apihelp-parse-param-onlypst": "Facer unha transformación antes de gardar (PST) a entrada, pero sen analizala. Devolve o mesmo wikitexto, despois de que a PST foi aplicada. Só válida cando se usa con <var>$1text</var>.",
"apihelp-parse-param-effectivelanglinks": "Inclúe ligazóns de idioma proporcionadas polas extensións (para usar con <kbd>$1prop=langlinks</kbd>).",
- "apihelp-parse-param-section": "Recuperar unicamente o contido deste número de sección ou cando <kbd>new</kbd> xera unha nova sección.\n\nA sección <kbd>new</kbd> só é atendida cando se especifica <var>text</var>.",
+ "apihelp-parse-param-section": "Analizar unicamente o contido deste número de sección.\n\nCando <kbd>nova</kbd>, analiza <var>$1text</var> e <var>$1sectiontitle</var> como se fose a engadir unha nova sección da páxina.\n\n<kbd>novo</kbd> só se permite cando especifica <var>text</var>.",
"apihelp-parse-param-sectiontitle": "Novo título de sección cando <var>section</var> é <kbd>new</kbd>.\n\nA diferenza da edición de páxinas, non se oculta no <var>summary</var> cando se omite ou está baleiro.",
"apihelp-parse-param-disablelimitreport": "Omitir o informe de límite (\"Informe de límite NewPP\") da saída do analizador.",
"apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> no seu lugar.",
"apihelp-query+pagepropnames-description": "Listar os nomes de todas as propiedades de páxina usados na wiki.",
"apihelp-query+pagepropnames-param-limit": "Máximo número de nomes a retornar.",
"apihelp-query+pagepropnames-example-simple": "Obter os dez primeiros nomes de propiedade.",
- "apihelp-query+pageprops-description": "Obter varias propiedades definidas no contido da páxina.",
- "apihelp-query+pageprops-param-prop": "Listar só esas propiedades. Útil para verificar se unha páxina concreta usa unha propiedade de páxina determinada.",
+ "apihelp-query+pageprops-description": "Obter varias propiedades de páxina definidas no contido da páxina.",
+ "apihelp-query+pageprops-param-prop": "Listar só estas propiedades de páxina (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina usados). Útil para verificar se as páxinas usan unha determinada propiedade de páxina.",
"apihelp-query+pageprops-example-simple": "Obter as propiedades para as páxinas <kbd>Main Page</kbd> e <kbd>MediaWiki</kbd>",
"apihelp-query+pageswithprop-description": "Mostrar a lista de páxinas que empregan unha propiedade determinada.",
- "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina pola que enumerar as páxinas.",
+ "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina para a que enumerar as páxinas (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina en uso).",
"apihelp-query+pageswithprop-param-prop": "Que información incluír:",
"apihelp-query+pageswithprop-paramvalue-prop-ids": "Engade o ID da páxina.",
"apihelp-query+pageswithprop-paramvalue-prop-title": "Engade o título e o ID do espazo de nomes da páxina.",
- "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade da páxina.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade de páxina.",
"apihelp-query+pageswithprop-param-limit": "Máximo número de páxinas a retornar.",
"apihelp-query+pageswithprop-param-dir": "En que dirección ordenar.",
"apihelp-query+pageswithprop-example-simple": "Lista as dez primeiras páxinas que usan <code>{{DISPLAYTITLE:}}</code>.",
"apihelp-setnotificationtimestamp-example-page": "Restaurar o estado de notificación para a <kbd>Páxina Principal</kbd>.",
"apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixar o selo de tempo de notificación para a <kbd>Main page</kbd> de forma que todas as edicións dende o 1 se xaneiro de 2012 queden sen revisar.",
"apihelp-setnotificationtimestamp-example-allpages": "Restaurar o estado de notificación para as páxinas no espazo de nomes de <kbd>{{ns:user}}</kbd>.",
+ "apihelp-stashedit-param-title": "Título da páxina que se está a editar.",
+ "apihelp-stashedit-param-section": "Número de selección. O <kbd>0</kbd> é para a sección superior, <kbd>novo</kbd> para unha sección nova.",
+ "apihelp-stashedit-param-sectiontitle": "Título para unha nova sección.",
"apihelp-stashedit-param-text": "Contido da páxina.",
"apihelp-stashedit-param-contentmodel": "Modelo de contido para o novo contido.",
"apihelp-stashedit-param-contentformat": "Formato de serialización de contido utilizado para o texto de entrada.",
"apihelp-query+pageprops-example-simple": "Holl de Eijeschaffte för di Sigge „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">Main Page</kbd>“ un „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">MediaWiki</kbd>“.",
"apihelp-query+pageswithprop-description": "Donn alle Sigge met bechtemmpte Sigge_Eijeschaff opleßte.",
"apihelp-query+pageswithprop-paramvalue-prop-ids": "Deiht de Kännong vun de Sigge derbei.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Deiht der Wäät för de Eijeschaff vun dä Sigg derbei.",
"apihelp-query+pageswithprop-param-limit": "De jrüüßte Zahl Sigge för ußzejävve.",
"apihelp-query+pageswithprop-param-dir": "En wälsche Reihjefollsch opleßte.",
"apihelp-query+pageswithprop-example-generator": "Holl zohsäzlejje Aanjahbe övver de eezde zehn Sigge, woh <code>__NOTOC__</code> dren vörkütt.",
"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}}:",
"apihelp-login-param-domain": "شپول (اختياري).",
"apihelp-login-example-login": "ننوتل.",
"apihelp-move-description": "يو مخ لېږدول.",
- "apihelp-query+search-example-simple": "د <kbd>مانا</kbd> پلټل.",
+ "apihelp-query+search-example-simple": "د <kbd>meaning</kbd> پلټل.",
"apihelp-query+search-example-text": "د <kbd>مانا</kbd> لپاره متنونه پلټل.",
"apihelp-query+watchlist-paramvalue-prop-title": "د يو مخ سرليک ورگډوي.",
"apihelp-tag-param-reason": "د بدلون سبب.",
public static function listExtensionActivatedTags() {
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'active-tags' ),
- function() {
+ function ( $oldValue, &$ttl, array &$setOpts ) {
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+
// Ask extensions which tags they consider active
$extensionActive = array();
Hooks::run( 'ChangeTagsListActive', array( &$extensionActive ) );
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'valid-tags-db' ),
- function() use ( $fname ) {
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE );
- $tags = $dbr->selectFieldValues(
- 'valid_tag', 'vt_tag', array(), $fname );
+
+ $setOpts += Database::getCacheSetOptions( $dbr );
+
+ $tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', array(), $fname );
return array_filter( array_unique( $tags ) );
},
public static function listExtensionDefinedTags() {
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'valid-tags-hook' ),
- function() {
+ function ( $oldValue, &$ttl, array &$setOpts ) {
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+
$tags = array();
Hooks::run( 'ListDefinedTags', array( &$tags ) );
return array_filter( array_unique( $tags ) );
$fname = __METHOD__;
$cachedStats = ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'change-tag-statistics' ),
- function() use ( $fname ) {
- $out = array();
-
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE, 'vslow' );
+
+ $setOpts += Database::getCacheSetOptions( $dbr );
+
$res = $dbr->select(
'change_tag',
array( 'ct_tag', 'hitcount' => 'count(*)' ),
array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' )
);
+ $out = array();
foreach ( $res as $row ) {
$out[$row->ct_tag] = $row->hitcount;
}
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function getSessionLagStatus() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
public function maxListLen() {
return $this->__call( __FUNCTION__, func_get_args() );
}
protected $mServer, $mUser, $mPassword, $mDBname;
+ /** @var BagOStuff APC cache */
+ protected $srvCache;
+
/** @var resource Database connection */
protected $mConn = null;
protected $mOpened = false;
*/
private $mTrxTimestamp = null;
+ /** @var float Lag estimate at the time of BEGIN */
+ private $mTrxSlaveLag = null;
+
/**
* Remembers the function name given for starting the most recent transaction via begin().
* Used to provide additional context for error reporting.
function __construct( array $params ) {
global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
+ $this->mTrxAtomicLevels = new SplStack;
+ $this->srvCache = ObjectCache::newAccelerator( 'hash' );
+
$server = $params['host'];
$user = $params['user'];
$password = $params['password'];
$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 );
$this->mTrxPreCommitCallbacks = array();
$this->mTrxShortId = wfRandomString( 12 );
$this->mTrxWriteDuration = 0.0;
+ // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
+ // Get an estimate of the slave lag before then, treating estimate staleness
+ // as lag itself just to be safe
+ $status = $this->getApproximateLagStatus();
+ $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
}
/**
return true;
}
+ /**
+ * Get the slave lag when the current transaction started
+ * or a general lag estimate if not transaction is active
+ *
+ * This is useful when transactions might use snapshot isolation
+ * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+ * is this lag plus transaction duration. If they don't, it is still
+ * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
+ * indication of the staleness of subsequent reads.
+ *
+ * @return array ('lag': seconds, 'since': UNIX timestamp of BEGIN)
+ * @since 1.27
+ */
+ public function getSessionLagStatus() {
+ return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
+ }
+
+ /**
+ * Get the slave lag when the current transaction started
+ *
+ * This is useful when transactions might use snapshot isolation
+ * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+ * is this lag plus transaction duration. If they don't, it is still
+ * safe to be pessimistic. This returns null if there is no transaction.
+ *
+ * @return array|null ('lag': seconds, 'since': UNIX timestamp of BEGIN)
+ * @since 1.27
+ */
+ public function getTransactionLagStatus() {
+ return $this->mTrxLevel
+ ? array( 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() )
+ : null;
+ }
+
+ /**
+ * Get a slave lag estimate for this server
+ *
+ * @return array ('lag': seconds, 'since': UNIX timestamp of estimate)
+ * @since 1.27
+ */
+ public function getApproximateLagStatus() {
+ return array(
+ 'lag' => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
+ 'since' => microtime( true )
+ );
+ }
+
+ /**
+ * Merge the result of getSessionLagStatus() for several DBs
+ * using the most pessimistic values to estimate the lag of
+ * any data derived from them in combination
+ *
+ * This is information is useful for caching modules
+ *
+ * @see WANObjectCache::set()
+ * @see WANObjectCache::getWithSetCallback()
+ *
+ * @param IDatabase $db1
+ * @param IDatabase ...
+ * @return array ('lag': highest lag, 'since': lowest estimate UNIX timestamp)
+ * @since 1.27
+ */
+ public static function getCacheSetOptions( IDatabase $db1 ) {
+ $res = array( 'lag' => 0, 'since' => INF );
+ foreach ( func_get_args() as $db ) {
+ /** @var IDatabase $db */
+ $status = $db->getSessionLagStatus();
+ $res['lag'] = max( $res['lag'], $status['lag'] );
+ $res['since'] = min( $res['since'], $status['since'] );
+ }
+
+ return $res;
+ }
+
/**
* Get slave lag. Currently supported only by MySQL.
*
}
}
}
+
+/**
+ * @since 1.27
+ */
+abstract class Database extends DatabaseBase {
+ // B/C until nothing type hints for DatabaseBase
+ // @TODO: finish renaming DatabaseBase => Database
+}
*/
class DBUnexpectedError extends DBError {
}
+
+/**
+ * @ingroup Database
+ */
+class DBReadOnlyError extends DBError {
+}
/**
* @ingroup Database
*/
-class DatabaseMssql extends DatabaseBase {
+class DatabaseMssql extends Database {
protected $mInsertId = null;
protected $mLastResult = null;
protected $mAffectedRows = null;
* @since 1.22
* @see Database
*/
-abstract class DatabaseMysqlBase extends DatabaseBase {
+abstract class DatabaseMysqlBase extends Database {
/** @var MysqlMasterPos */
protected $lastKnownSlavePos;
/** @var string Method to detect slave lag */
protected $lagDetectionMethod;
- /** @var BagOStuff APC cache */
- protected $srvCache;
-
/** @var string|null */
private $serverVersion = null;
$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
? $params['lagDetectionMethod']
: 'Seconds_Behind_Master';
-
- $this->srvCache = ObjectCache::newAccelerator( 'hash' );
}
/**
return false;
}
+ public function getApproximateLagStatus() {
+ if ( $this->lagDetectionMethod === 'pt-heartbeat' ) {
+ // Disable caching since this is fast enough and we don't wan't
+ // to be *too* pessimistic by having both the cache TTL and the
+ // pt-heartbeat interval count as lag in getSessionLagStatus()
+ return parent::getApproximateLagStatus();
+ }
+
+ $key = wfGlobalCacheKey( 'mysql-lag', $this->getServer() );
+ $approxLag = $this->srvCache->get( $key );
+ if ( !$approxLag ) {
+ $approxLag = parent::getApproximateLagStatus();
+ $this->srvCache->set( $key, $approxLag, 1 );
+ }
+
+ return $approxLag;
+ }
+
/**
* Wait for the slave to catch up to a given master position.
* @todo Return values for this and base class are rubbish
/**
* @ingroup Database
*/
-class DatabaseOracle extends DatabaseBase {
+class DatabaseOracle extends Database {
/** @var resource */
protected $mLastResult = null;
/**
* @ingroup Database
*/
-class DatabasePostgres extends DatabaseBase {
+class DatabasePostgres extends Database {
/** @var resource */
protected $mLastResult = null;
/**
* @ingroup Database
*/
-class DatabaseSqlite extends DatabaseBase {
+class DatabaseSqlite extends Database {
/** @var bool Whether full text is enabled */
private static $fulltextEnabled = null;
*/
public function getLag();
+ /**
+ * Get the slave lag when the current transaction started
+ * or a general lag estimate if not transaction is active
+ *
+ * This is useful when transactions might use snapshot isolation
+ * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+ * is this lag plus transaction duration. If they don't, it is still
+ * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
+ * indication of the staleness of subsequent reads.
+ *
+ * @return array ('lag': seconds, 'since': UNIX timestamp of BEGIN)
+ * @since 1.27
+ */
+ public function getSessionLagStatus();
+
/**
* Return the maximum number of items allowed in a list, or 0 for unlimited.
*
$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;
}
$this->$key = $conf[$key];
}
}
-
- // Check for read-only mode
- $section = $this->getSectionForWiki();
- if ( !empty( $this->readOnlyBySection[$section] ) ) {
- global $wgReadOnly;
- $wgReadOnly = $this->readOnlyBySection[$section];
- }
}
/**
$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;
}
}
/**
+ * @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() {
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
* Get a slave database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlave( $cluster ) {
global $wgDefaultExternalStore;
* 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;
* 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 ) {
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 ) );
);
}
- $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." );
}
$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,
* @since 1.20
*/
class DBFileJournal extends FileJournal {
- /** @var DatabaseBase */
+ /** @var IDatabase */
protected $dbw;
protected $wiki = false; // string; wiki DB name
/**
* Get a master connection to the logging DB
*
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getMasterDB() {
* Get (or reuse) a connection to a lock DB
*
* @param string $lockDb
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getConnection( $lockDb ) {
* 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 ) {
}
/**
/**
* @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;" );
}
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlaveDB() {
return $this->getMasterDB();
}
/**
- * @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 );
function ( $oldValue, &$ttl, array &$setOpts ) use ( $that, $title ) {
$dbr = $that->getSlaveDB(); // possibly remote DB
- $setOpts = array( 'since' => $dbr->trxTimestamp() );
+ $setOpts += Database::getCacheSetOptions( $dbr );
if ( $title instanceof Title ) {
$row = $dbr->selectRow(
// 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' ) );
}
}
* @param string $end Only revisions newer than $end will be returned
* @param bool $inc Include the endpoints of the time range
*
- * @return array
+ * @return File[]
*/
function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
return array();
// Cache presence for 1 week and negatives for 1 day
$ttl = $this->fileExists ? 86400 * 7 : 86400;
- $opts = array( 'since' => wfGetDB( DB_SLAVE )->trxTimestamp() );
+ $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
}
* 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
* @param int $start Optional: Timestamp, start from
* @param int $end Optional: Timestamp, end at
* @param bool $inc
- * @return array
+ * @return OldLocalFile[]
*/
function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
$dbr = $this->repo->getSlaveDB();
$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',
__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;
}
* @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 );
), __METHOD__
);
- $dbw->commit( __METHOD__ );
-
return true;
}
function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
$dbr = wfGetDB( DB_SLAVE );
+ $setOpts += Database::getCacheSetOptions( $dbr );
+
$row = $dbr->selectRow(
'interwiki',
Interwiki::selectFields(),
__METHOD__
);
- $setOpts = array( 'since' => $dbr->trxTimestamp() );
-
return $row ? (array)$row : '!NONEXISTENT';
},
$wgInterwikiExpiry
// 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 );
}
}
public function run() {
+ /** @noinspection PhpUnusedLocalVariableInspection */
$scope = RequestContext::importScopedSession( $this->params['session'] );
$context = RequestContext::getMain();
$user = $context->getUser();
$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
'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 );
}
public function run() {
+ /** @noinspection PhpUnusedLocalVariableInspection */
$scope = RequestContext::importScopedSession( $this->params['session'] );
$context = RequestContext::getMain();
$user = $context->getUser();
'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 );
--- /dev/null
+<?php
+/**
+ * APC-backed function memoization
+ *
+ * This class provides memoization for pure functions. A function is pure
+ * if its result value depends on nothing other than its input parameters
+ * and if invoking it does not cause any side-effects.
+ *
+ * The first invocation of the memoized callable with a particular set of
+ * arguments will be delegated to the underlying callable. Repeat invocations
+ * with the same input parameters will be served from APC.
+ *
+ * @par Example:
+ * @code
+ * $memoizedStrrev = new MemoizedCallable( 'range' );
+ * $memoizedStrrev->invoke( 5, 8 ); // result: array( 5, 6, 7, 8 )
+ * $memoizedStrrev->invokeArgs( array( 5, 8 ) ); // same
+ * MemoizedCallable::call( 'range', array( 5, 8 ) ); // same
+ * @endcode
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Ori Livneh
+ * @since 1.27
+ */
+class MemoizedCallable {
+
+ /** @var callable */
+ private $callable;
+
+ /** @var string Unique name of callable; used for cache keys. */
+ private $callableName;
+
+ /**
+ * Constructor.
+ *
+ * @throws InvalidArgumentException if $callable is not a callable.
+ * @param callable $callable Function or method to memoize.
+ * @param int $ttl TTL in seconds. Defaults to 3600 (1hr). Capped at 86400 (24h).
+ */
+ public function __construct( $callable, $ttl = 3600 ) {
+ if ( !is_callable( $callable, false, $this->callableName ) ) {
+ throw new InvalidArgumentException(
+ 'Argument 1 passed to MemoizedCallable::__construct() must ' .
+ 'be an instance of callable; ' . gettype( $callable ) . ' given'
+ );
+ }
+
+ if ( $this->callableName === 'Closure::__invoke' ) {
+ // Differentiate anonymous functions from one another
+ $this->callableName .= uniqid();
+ }
+
+ $this->callable = $callable;
+ $this->ttl = min( max( $ttl, 1 ), 86400 );
+ }
+
+ /**
+ * Fetch the result of a previous invocation from APC.
+ *
+ * @param string $key
+ * @param bool &$success
+ */
+ protected function fetchResult( $key, &$success ) {
+ $success = false;
+ if ( function_exists( 'apc_fetch' ) ) {
+ return apc_fetch( $key, $success );
+ }
+ return false;
+ }
+
+ /**
+ * Store the result of an invocation in APC.
+ *
+ * @param string $key
+ * @param mixed $result
+ */
+ protected function storeResult( $key, $result ) {
+ if ( function_exists( 'apc_store' ) ) {
+ apc_store( $key, $result, $this->ttl );
+ }
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * @throws InvalidArgumentException If parameters list contains non-scalar items.
+ * @param array $args Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invokeArgs( Array $args = array() ) {
+ foreach ( $args as $arg ) {
+ if ( $arg !== null && !is_scalar( $arg ) ) {
+ throw new InvalidArgumentException(
+ 'MemoizedCallable::invoke() called with non-scalar ' .
+ 'argument'
+ );
+ }
+ }
+
+ $hash = md5( serialize( $args ) );
+ $key = __CLASS__ . ':' . $this->callableName . ':' . $hash;
+ $success = false;
+ $result = $this->fetchResult( $key, $success );
+ if ( !$success ) {
+ $result = call_user_func_array( $this->callable, $args );
+ $this->storeResult( $key, $result );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * Like MemoizedCallable::invokeArgs(), but variadic.
+ *
+ * @param mixed ...$params Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invoke() {
+ return $this->invokeArgs( func_get_args() );
+ }
+
+ /**
+ * Shortcut method for creating a MemoizedCallable and invoking it
+ * with the specified arguments.
+ *
+ * @param callable $callable
+ * @param array $args
+ * @param int $ttl
+ */
+ public static function call( $callable, Array $args = array(), $ttl = 3600 ) {
+ $instance = new self( $callable, $ttl );
+ return $instance->invokeArgs( $args );
+ }
+}
protected $maxConnsPerHost = 50;
/** @var string|null proxy */
protected $proxy;
+ /** @var string */
+ protected $userAgent = 'wikimedia/multi-http-client v1.0';
/**
* @param array $options
* - proxy : HTTP proxy to use
* - usePipelining : whether to use HTTP pipelining if possible (for all hosts)
* - maxConnsPerHost : maximum number of concurrent connections (per host)
+ * - userAgent : The User-Agent header value to send
* @throws Exception
*/
public function __construct( array $options ) {
}
}
static $opts = array(
- 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy'
+ 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent'
);
foreach ( $opts as $key ) {
if ( isset( $options[$key] ) ) {
$req['headers']['content-length'] = 0;
}
+ if ( !isset( $req['headers']['user-agent'] ) ) {
+ $req['headers']['user-agent'] = $this->userAgent;
+ }
+
$headers = array();
foreach ( $req['headers'] as $name => $value ) {
if ( strpos( $name, ': ' ) ) {
* 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.
*
**/
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;
}
/** Bitfield constants for get()/getMulti() */
const READ_LATEST = 1; // use latest data for replicated stores
+ const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
public function __construct( array $params = array() ) {
if ( isset( $params['logger'] ) ) {
}
/**
- * Get an item with the given key. Returns false if it does not exist.
+ * Get an item with the given key, regenerating and setting it if not found
+ *
+ * If the callback returns false, then nothing is stored.
+ *
* @param string $key
- * @param mixed $casToken [optional]
- * @param integer $flags Bitfield; supports READ_LATEST [optional]
- * @return mixed Returns false on failure
+ * @param int $ttl Time-to-live (seconds)
+ * @param callable $callback Callback that derives the new value
+ * @return mixed The cached value if found or the result of $callback otherwise
+ * @since 1.27
+ */
+ final public function getWithSetCallback( $key, $ttl, $callback ) {
+ $value = $this->get( $key );
+
+ if ( $value === false ) {
+ if ( !is_callable( $callback ) ) {
+ throw new InvalidArgumentException( "Invalid cache miss callback provided." );
+ }
+ $value = call_user_func( $callback );
+ if ( $value !== false ) {
+ $this->set( $key, $value, $ttl );
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get an item with the given key
+ *
+ * If the key includes a determistic input hash (e.g. the key can only have
+ * the correct value) or complete staleness checks are handled by the caller
+ * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
+ * This lets tiered backends know they can safely upgrade a cached value to
+ * higher tiers using standard TTLs.
+ *
+ * @param string $key
+ * @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 );
/**
- * Set an item.
+ * @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
+ *
* @param string $key
* @param mixed $value
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
abstract public function set( $key, $value, $exptime = 0 );
/**
- * Delete an item.
+ * Delete an item
+ *
* @param string $key
* @return bool True if the item was deleted or not found, false on failure
*/
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)
}
}
$this->clearLastError();
- $currentValue = $this->get( $key );
+ $currentValue = $this->get( $key, BagOStuff::READ_LATEST );
if ( $this->getLastError() ) {
$success = false;
} else {
* @ingroup Cache
*/
class EmptyBagOStuff extends BagOStuff {
- public function get( $key, &$casToken = null, $flags = 0 ) {
+ protected function doGet( $key, $flags = 0 ) {
return false;
}
return true;
}
- public function get( $key, &$casToken = null, $flags = 0 ) {
+ protected function doGet( $key, $flags = 0 ) {
if ( !isset( $this->bag[$key] ) ) {
return false;
}
return false;
}
- $casToken = $this->bag[$key][0];
-
return $this->bag[$key][0];
}
$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 ) {
protected $lastRelayError = self::ERR_NONE;
/** Max time expected to pass between delete() and DB commit finishing */
- const MAX_COMMIT_DELAY = 1;
- /** Max expected replication lag for a reasonable storage setup */
- const MAX_REPLICA_LAG = 7;
+ const MAX_COMMIT_DELAY = 3;
+ /** Max replication lag before applying TTL_LAGGED to set() */
+ const MAX_REPLICA_LAG = 5;
/** Max time since snapshot transaction start to avoid no-op of set() */
- const MAX_SNAPSHOT_LAG = 6;
+ const MAX_SNAPSHOT_LAG = 5;
/** Seconds to tombstone keys on delete() */
- const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG
+ const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG + 1
/** Seconds to keep dependency purge keys around */
const CHECK_KEY_TTL = 31536000; // 1 year
/** 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;
const TTL_UNCACHEABLE = -1;
/** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
const TSE_NONE = -1;
+ /** Max TTL to store keys when a data sourced is lagged */
+ const TTL_LAGGED = 30;
/** Cache format version number */
const VERSION = 1;
* - 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 = Database::getCacheSetOptions( $dbr );
* // Fetch the row from the DB
* $row = $dbr->selectRow( ... );
* $key = wfMemcKey( 'building', $buildingId );
- * // Give the age of the transaction snapshot the data came from
- * $opts = array( 'since' => $dbr->trxTimestamp() );
- * $cache->set( $key, $row, 86400, $opts );
+ * $cache->set( $key, $row, 86400, $setOpts );
* @endcode
*
* @param string $key Cache key
* @param mixed $value
* @param integer $ttl Seconds to live [0=forever]
* @param array $opts Options map:
+ * - lag : Seconds of slave lag. Typically, this is either the slave lag
+ * before the data was read or, if applicable, the slave lag before
+ * the snapshot-isolated transaction the data was read from started.
+ * [Default: 0 seconds]
* - since : UNIX timestamp of the data in $value. Typically, this is either
* the current time the data was read or (if applicable) the time when
* the snapshot-isolated transaction the data was read from started.
final public function set( $key, $value, $ttl = 0, array $opts = array() ) {
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
$age = isset( $opts['since'] ) ? max( 0, microtime( true ) - $opts['since'] ) : 0;
+ $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
+
+ if ( $lag > self::MAX_REPLICA_LAG ) {
+ // Too much lag detected; lower TTL so it converges faster
+ $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
+ }
if ( $age > self::MAX_SNAPSHOT_LAG ) {
if ( $lockTSE >= 0 ) {
* 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
* // Key to store the cached value under
* wfMemcKey( 'cat-attributes', $catId ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
- * // Fetch row from the DB
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
- * $row = $dbr->selectRow( ... );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * return $row;
+ * return $dbr->selectRow( ... );
* },
* // Time-to-live (seconds)
* 60
* // Key to store the cached value under
* wfMemcKey( 'site-cat-config' ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
- * // Fetch row from the DB
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
- * $config = CatConfig::newFromRow( $dbr->selectRow( ... ) );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
- *
- * return $config;
+ * return CatConfig::newFromRow( $dbr->selectRow( ... ) );
* },
* // Time-to-live (seconds)
* 86400,
* // Key to store the cached value under
* wfMemcKey( 'cat-state', $cat->getId() ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* // Determine new value from the DB
* $dbr = wfGetDB( DB_SLAVE );
- * $state = CatState::newFromResults( $dbr->select( ... ) );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
- * return $state;
+ * return CatState::newFromResults( $dbr->select( ... ) );
* },
* // Time-to-live (seconds)
* 900,
* // Key to store the cached value under
* wfMemcKey( 'cat-last-actions', 100 ),
* // Function that derives the new key value
- * function( $oldValue, &$ttl, array &$setOpts ) {
+ * function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
+ * // Account for any snapshot/slave lag
+ * $setOpts += Database::getCacheSetOptions( $dbr );
+ *
* // Start off with the last cached list
* $list = $oldValue ?: array();
* // Fetch the last 100 relevant rows in descending order;
* // only fetch rows newer than $list[0] to reduce scanning
* $rows = iterator_to_array( $dbr->select( ... ) );
* // Merge them and get the new "last 100" rows
- * $list = array_slice( array_merge( $new, $list ), 0, 100 );
- *
- * // Set age of the transaction snapshot the data came from
- * $setOpts = array( 'since' => $dbr->trxTimestamp() );
- *
- * return $list;
+ * return array_slice( array_merge( $new, $list ), 0, 100 );
* },
* // Time-to-live (seconds)
* 10,
* @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;
* @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 ) ) {
* @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 );
}
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
* -------------------
$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
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 );
}
$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 );
/** @var bool Use async secondary writes */
protected $asyncWrites = false;
+ /** Idiom for "write to all backends" */
+ const ALL = INF;
+
+ const UPGRADE_TTL = 3600; // TTL when a key is copied to a higher cache tier
+
/**
* $params include:
* - caches: This should have a numbered array of cache parameter
* @param bool $debug
*/
public function setDebug( $debug ) {
- $this->doWrite( 'setDebug', $debug );
+ $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 ) {
- return $value;
+ break;
}
+ ++$misses;
+ }
+
+ if ( $value !== false
+ && $misses > 0
+ && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
+ ) {
+ $this->doWrite( $misses, 'set', $key, $value, self::UPGRADE_TTL );
}
- return false;
+
+ return $value;
}
/**
* @return bool
*/
public function set( $key, $value, $exptime = 0 ) {
- return $this->doWrite( 'set', $key, $value, $exptime );
+ return $this->doWrite( self::ALL, 'set', $key, $value, $exptime );
}
/**
* @return bool
*/
public function delete( $key ) {
- return $this->doWrite( 'delete', $key );
+ return $this->doWrite( self::ALL, 'delete', $key );
}
/**
* @return bool
*/
public function add( $key, $value, $exptime = 0 ) {
- return $this->doWrite( 'add', $key, $value, $exptime );
+ return $this->doWrite( self::ALL, 'add', $key, $value, $exptime );
}
/**
* @return bool|null
*/
public function incr( $key, $value = 1 ) {
- return $this->doWrite( 'incr', $key, $value );
+ return $this->doWrite( self::ALL, 'incr', $key, $value );
}
/**
* @return bool
*/
public function decr( $key, $value = 1 ) {
- return $this->doWrite( 'decr', $key, $value );
+ return $this->doWrite( self::ALL, 'decr', $key, $value );
}
/**
* @return bool Success
*/
public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
- return $this->doWrite( 'merge', $key, $callback, $exptime );
+ return $this->doWrite( self::ALL, 'merge', $key, $callback, $exptime );
}
public function getLastError() {
}
/**
+ * Apply a write method to the first $count backing caches
+ *
+ * @param integer $count
* @param string $method
+ * @param mixed ...
* @return bool
*/
- protected function doWrite( $method /*, ... */ ) {
+ protected function doWrite( $count, $method /*, ... */ ) {
$ret = true;
- $args = func_get_args();
- array_shift( $args );
+ $args = array_slice( func_get_args(), 2 );
foreach ( $this->caches as $i => $cache ) {
+ if ( $i >= $count ) {
+ break; // ignore the lower tiers
+ }
+
if ( $i == 0 || !$this->asyncWrites ) {
// First store or in sync mode: write now and get result
if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
}
}
- 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;
* Get a connection to the specified database
*
* @param int $serverIndex
- * @return DatabaseBase
+ * @return IDatabase
* @throws MWException
*/
protected function getDB( $serverIndex ) {
}
}
- 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];
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @param string $exptime
* @return bool
*/
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @return string
*/
protected function getMaxDateTime( $db ) {
$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 {
// Images
if ( $title->getNamespace() == NS_FILE ) {
- $update = new HTMLCacheUpdate( $title, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
}
// User talk pages
/**
* Get the Database object in use
*
- * @return DatabaseBase
+ * @return IDatabase
*/
public function getDatabase() {
return $this->mDb;
}
// Determine the options which affect this article
- $optionsKey = $this->mMemc->get( $this->getOptionsKey( $article ) );
+ $casToken = null;
+ $optionsKey = $this->mMemc->get(
+ $this->getOptionsKey( $article ), $casToken, BagOStuff::READ_VERIFIED );
if ( $optionsKey instanceof CacheTime ) {
if ( !$useOutdated && $optionsKey->expired( $article->getTouched() ) ) {
wfIncrStats( "pcache.miss.expired" );
return false;
}
- $value = $this->mMemc->get( $parserOutputKey );
+ $casToken = null;
+ $value = $this->mMemc->get( $parserOutputKey, $casToken, BagOStuff::READ_VERIFIED );
if ( !$value ) {
wfDebug( "ParserOutput cache miss.\n" );
wfIncrStats( "pcache.miss.absent" );
$this->missingLocalFileRefs[] = $file;
}
}
- return CSSMin::remap(
- $style, $localDir, $remoteDir, true
- );
+ return MemoizedCallable::call( 'CSSMin::remap',
+ array( $style, $localDir, $remoteDir, true ) );
}
/**
'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
'wgResourceLoaderLegacyModules' => self::getLegacyModules(),
- 'wgRemoteUploadTarget' => $conf->get( 'RemoteUploadTarget' ),
+ 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
);
Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) );
}
/**
- * @return Title|null Title object (pagename+fragment) for the section, null if none or not supported
+ * @return Title|null Title object (pagename+fragment) for the section,
+ * null if none or not supported
*/
function getSectionTitle() {
return null;
*/
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;
}
/**
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.
*
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',
return true;
}
- $dbw = $this->sitesTable->getWriteDbConnection();
+ $dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$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 );
}
}
}
* @return bool Success
*/
public function clear() {
- $dbw = $this->sitesTable->getWriteDbConnection();
+ $dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$ok = $dbw->delete( 'sites', '*', __METHOD__ );
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_'
- );
- }
-
}
* @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 );
}
* Failures here must throw subclasses of ErrorPageError.
* @param User $user
* @throws UserBlockedError
- * @return bool True
*/
protected function checkExecutePermissions( User $user ) {
$this->checkPermissions();
if ( $this->requiresWrite() ) {
$this->checkReadOnly();
}
-
- return true;
}
/**
throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
}
- return parent::checkExecutePermissions( $user );
+ parent::checkExecutePermissions( $user );
}
protected function getFormFields() {
$out->addHtml( "</div>" );
- Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out ) );
+ Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out, $term ) );
}
}
protected function showHistory() {
+ $this->checkReadOnly();
+
$out = $this->getOutput();
if ( $this->mAllowed ) {
$out->addModules( 'mediawiki.special.undelete' );
throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
}
- if ( wfReadOnly() ) {
- throw new ReadOnlyError;
- }
+ $this->checkReadOnly();
$out = $this->getOutput();
$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
class BatchRowIterator implements RecursiveIterator {
/**
- * @var DatabaseBase $db The database to read from
+ * @var IDatabase $db The database to read from
*/
protected $db;
/**
* @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;
/**
* @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 );
/**
* @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 );
/**
* @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
*/
class BatchRowWriter {
/**
- * @var DatabaseBase $db The database to write to
+ * @var IDatabase $db The database to write to
*/
protected $db;
* @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;
}
return $hash;
}
- $contents = MediaWiki\quietCall( 'file_get_contents', $filePath );
+ $contents = file_get_contents( $filePath );
if ( $contents === false ) {
return false;
}
$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 );
return $instance->getFileContentsHashInternal( $filePath, $algo ) ?: '';
}, $filePaths );
+ MediaWiki\restoreWarnings();
+
$hashes = implode( '', $hashes );
return $hashes ? hash( $algo, $hashes ) : false;
}
"changeemail-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
"changeemail-oldemail": "Цяперашні адрас электроннай пошты:",
"changeemail-newemail": "Новы адрас электроннай пошты:",
+ "changeemail-newemail-help": "Поле трэба пакінуць пустым, калі вы хочаце выдаліць свой адрас электроннай пошты. Пасьля выдаленьня вы ня зможаце ануляваць забыты пароль і ня будзеце атрымліваць лісты электроннай пошты з гэтай вікі.",
"changeemail-none": "(няма)",
"changeemail-password": "Ваш пароль у {{GRAMMAR:месны|{{SITENAME}}}}:",
"changeemail-submit": "Зьмяніць адрас электроннай пошты",
"nopagetext": "Пазначанай мэтавай старонкі не існуе.",
"pager-newer-n": "$1 {{PLURAL:$1|навейшая|навейшыя|навейшых}}",
"pager-older-n": "$1 {{PLURAL:$1|старэйшая|старэйшыя|старэйшых}}",
- "suppress": "Ð Ñ\8dвÑ\96заваÑ\86Ñ\8c",
+ "suppress": "Ð\9fадавÑ\96Ñ\86Ñ\8c вÑ\8dÑ\80Ñ\81Ñ\96Ñ\8e",
"querypage-disabled": "Гэта спэцыяльная старонка адключаная для падвышэньня прадукцыйнасьці",
"apihelp": "Даведка API",
"apihelp-no-such-module": "Модуль «$1» ня знойдзены.",
"nstab-template": "обраꙁьць",
"nstab-help": "страница помощи",
"nstab-category": "катигорїꙗ",
+ "mainpage-nstab": "главьна страница",
"nosuchspecialpage": "си нарочнꙑ страницѧ нѣстъ",
"error": "блаꙁна",
"internalerror": "вънѫтрѣнꙗ блаꙁна",
"block-log-flags-anononly": "тъкъмо анѡнѷмьнꙑ польꙃєватєлє",
"move-page": "прѣимєнованиѥ ⁖ $1 ⁖",
"move-page-legend": "страницѧ прѣимєнованиѥ",
- "movearticle": "страница :",
"newtitle": "ново имѧ :",
"move-watch": "си страницѧ блюдєниѥ",
"movepagebtn": "прѣимєнованиѥ",
"nstab-template": "Şablon",
"nstab-help": "Pela peşti",
"nstab-category": "Kategoriye",
+ "mainpage-nstab": "Pela seri",
"nosuchaction": "Fealiyeto wınasi çıniyo",
"nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
"nosuchspecialpage": "Pela xasa wınasiye çıniya",
"tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
"tooltip-pt-logout": "Bıveciye",
"tooltip-ca-talk": "Zerrekê pele sero werênayış",
- "tooltip-ca-edit": "Tı şenay na pele bıvurnê. Kerem ke, qeydkerdış ra ver gocega verqayti bıgurene.",
+ "tooltip-ca-edit": "Ena pele bıvurne",
"tooltip-ca-addsection": "Zu bınnusteya newi ak",
"tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê",
"tooltip-ca-history": "Versiyonê verênê ena pele",
"api-error-badaccess-groups": "Ena wiki de dosya barkerdışi rê mısade nêdeyêno.",
"api-error-badtoken": "Xetaya zerreki: Antışo xırabın.",
"api-error-copyuploaddisabled": "URL barkerdış ena waster dı qefılyayo.",
- "api-error-duplicate": "Ena {{PLURAL:$1|ze ke zeq|biya zey dosya da}} zeq wesiqa biya wendeyê.",
+ "api-error-duplicate": "Pele de xora be nê zerreki ra {{PLURAL:$1|dosyaya bine esta|dosyeyê bini estê}}.",
"api-error-duplicate-archive": "Ena {{PLURAL:$1|vurneyaya zey na dosya|zerrey cı zey dosya}} aseno,feqet {{PLURAL:$1|ena dosya|tewr veri}} besterneyaya.",
"api-error-empty-file": "Dosyaya ke şıma rışta venga.",
"api-error-emptypage": "Newi, pelaya veng vıraştışi rê mısade nêdeyêno.",
"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.",
"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.",
"viewsource": "Ver o código fonte",
"viewsource-title": "Ver o código fonte de \"$1\"",
"actionthrottled": "Acción limitada",
- "actionthrottledtext": "Como unha medida de loita contra o ''spam'', limítase a realización desta acción a un número determinado de veces nun curto espazo de tempo, e vostede superou este límite.\nInténteo de novo nuns minutos.",
+ "actionthrottledtext": "Como medida contra os abusos, a acción que está realizando está limitada a un número determinado de veces nun periodo curto de tempo, e superou ese límite.\nInténteo de novo nuns minutos.",
"protectedpagetext": "Esta páxina foi protexida para evitar a edición e outras accións.",
"viewsourcetext": "Pode ver e copiar o código fonte desta páxina.",
"viewyourtext": "Pode ver e copiar o código fonte <strong>das súas edicións</strong> nesta páxina.",
"createacct-captcha": "Comprobación de seguridade",
"createacct-imgcaptcha-ph": "Insira o texto que ve enriba",
"createacct-submit": "Crear a conta",
- "createacct-another-submit": "Crear outra conta",
+ "createacct-another-submit": "Crear conta",
"createacct-benefit-heading": "Xente coma vostede elabora {{SITENAME}}.",
"createacct-benefit-body1": "{{PLURAL:$1|edición|edicións}}",
"createacct-benefit-body2": "{{PLURAL:$1|páxina|páxinas}}",
"passwordreset-emailtext-ip": "Alguén (probablemente vostede, desde o enderezo IP $1) solicitou o restablecemento do seu\ncontrasinal de {{SITENAME}} ($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
"passwordreset-emailtext-user": "O usuario $1 solicitou o restablecemento do contrasinal de {{SITENAME}}\n($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
"passwordreset-emailelement": "Nome de usuario: \n$1\n\nContrasinal temporal: \n$2",
- "passwordreset-emailsent": "Enviouse o correo electrónico de restablecemento do contrasinal.",
+ "passwordreset-emailsent": "Se esta é unha dirección de correo electrónico rexistrada para a túa conta, entón enviarase un correo electrónico para o restablecemento do teu contrasinal.",
"passwordreset-emailsent-capture": "Enviouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación.",
"passwordreset-emailerror-capture": "Xerouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación, pero o envío {{GENDER:$2|ao usuario|á usuaria}} fallou: $1",
- "changeemail": "Cambiar o enderezo de correo electrónico",
- "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio.",
+ "changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
+ "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio. Se vostede quere eliminar a asociación da dirección de correo electrónico da súa conta, deixe en branco a nova dirección de correo electrónico cando envíe o formulario.",
"changeemail-no-info": "Debe rexistrarse para acceder directamente a esta páxina.",
"changeemail-oldemail": "Enderezo de correo electrónico actual:",
"changeemail-newemail": "Novo enderezo de correo electrónico:",
"permissionserrorstext-withaction": "Non ten os permisos necesarios para $2, {{PLURAL:$1|pola seguinte razón|polas seguintes razóns}}:",
"recreate-moveddeleted-warn": "'''Atención: Vai volver crear unha páxina que xa foi eliminada anteriormente.'''\n\nDebería considerar se é apropiado continuar a editar esta páxina.\nVelaquí están o rexistro de borrados e mais o de traslados desta páxina, por se quere consultalos:",
"moveddeleted-notice": "Esta páxina foi borrada.\nA continuación pódese ver o rexistro de borrados e traslados desta páxina, por se quere consultalos.",
+ "moveddeleted-notice-recent": "Sentímolo, esta página foi borrada recentemente (dentro das últimas 24 horas).\nO rexistro de borrado e traslado da páxina amósanse abaixo como referencia.",
"log-fulllog": "Ver o rexistro completo",
"edit-hook-aborted": "A edición foi abortada polo asociador.\nEste non deu ningunha explicación.",
"edit-gone-missing": "Non se pode actualizar a páxina.\nSemella que foi borrada.",
"mergehistory-go": "Mostrar as edicións que se poden fusionar",
"mergehistory-submit": "Fusionar as revisións",
"mergehistory-empty": "Non hai revisións que se poidan fusionar.",
- "mergehistory-done": "{{PLURAL:$3|Unha revisión|$3 revisións}} de \"$1\" {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con \"[[:$2]]\".",
+ "mergehistory-done": "$3 {{PLURAL:$3|revisión|revisións}} de $1 {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con [[:$2]].",
"mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e data.",
"mergehistory-fail-toobig": "Non se puido fusionar o historial, xa que supón trasladar máis revisións que o límite de $1 {{PLURAL:$1|revisión|revisións}}.",
"mergehistory-no-source": "Non existe a páxina de orixe \"$1\".",
"prefs-watchlist-token": "Pase para a lista de vixilancia:",
"prefs-misc": "Preferencias varias",
"prefs-resetpass": "Cambiar o contrasinal",
- "prefs-changeemail": "Cambiar o enderezo de correo electrónico",
+ "prefs-changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
"prefs-setemail": "Establecer un enderezo de correo electrónico",
"prefs-email": "Opcións de correo electrónico",
"prefs-rendering": "Aparencia",
"group-bot": "Bots",
"group-sysop": "Administradores",
"group-bureaucrat": "Burócratas",
- "group-suppress": "Supervisores",
+ "group-suppress": "Supresores",
"group-all": "(todos)",
"group-user-member": "{{GENDER:$1|usuario|usuaria}}",
"group-autoconfirmed-member": "{{GENDER:$1|usuario autoconfirmado|usuaria autoconfirmada}}",
"group-bot-member": "{{GENDER:$1|bot}}",
"group-sysop-member": "{{GENDER:$1|administrador|administradora}}",
"group-bureaucrat-member": "{{GENDER:$1|burócrata}}",
- "group-suppress-member": "{{GENDER:$1|supervisor|supervisora}}",
+ "group-suppress-member": "{{GENDER:$1|supresor|supresora}}",
"grouppage-user": "{{ns:project}}:Usuarios",
"grouppage-autoconfirmed": "{{ns:project}}:Usuarios autoconfirmados",
"grouppage-bot": "{{ns:project}}:Bots",
"grouppage-sysop": "{{ns:project}}:Administradores",
"grouppage-bureaucrat": "{{ns:project}}:Burócratas",
- "grouppage-suppress": "{{ns:project}}:Supervisores",
+ "grouppage-suppress": "{{ns:project}}:Supresores",
"right-read": "Ler páxinas",
"right-edit": "Editar páxinas",
"right-createpage": "Crear páxinas (que non son de conversa)",
"recentchanges-page-added-to-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} engadíronse á categoría",
"recentchanges-page-removed-from-category": "\"[[:$1]]\" eliminouse da categoría",
"recentchanges-page-removed-from-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} elimináronse da categoría",
+ "autochange-username": "Cambio automático de MediaWiki",
"upload": "Subir un ficheiro",
"uploadbtn": "Subir un ficheiro",
"reuploaddesc": "Cancelar a carga e volver ao formulario de carga",
"upload-options": "Opcións de carga",
"watchthisupload": "Vixiar este ficheiro",
"filewasdeleted": "Un ficheiro con ese nome foi cargado con anterioridade e a continuación borrado.\nDebe comprobar o $1 antes de proceder a cargalo outra vez.",
+ "filename-thumb-name": "Semella que este título é dunha miniatura. Non cargue miniaturas no wiki do que as sacou. Se non é así, corrixa o nome do ficheiro para que sexa máis significativo e non teña o prefixo das miniaturas.",
"filename-bad-prefix": "O nome do ficheiro que está cargando comeza con '''\"$1\"''', que é un típico nome non descritivo asignado automaticamente polas cámaras dixitais.\nPor favor, escolla un nome máis descritivo para o seu ficheiro.",
"filename-prefix-blacklist": " #<!-- Deixe esta liña tal e como está --> <pre>\n# A sintaxe é a seguinte:\n# * Todo o que vaia despois dun carácter \"#\" ata o final da liña é un comentario\n# * Toda liña que non estea en branco é un prefixo para os nomes típicos dos ficheiros asignados automaticamente polas cámaras dixitais\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # algúns teléfonos móbiles\nIMG # xenérico\nJD # Jenoptik\nMGP # Pentax\nPICT # varios\n #</pre> <!-- Deixe esta liña tal e como está -->",
"upload-success-subj": "A carga realizouse correctamente",
"upload-form-label-infoform-description": "Descrición",
"upload-form-label-usage-title": "Uso",
"upload-form-label-usage-filename": "Nome do ficheiro",
+ "foreign-structured-upload-form-label-own-work": "Isto é o meu propio traballo",
+ "foreign-structured-upload-form-label-infoform-categories": "Categorías",
+ "foreign-structured-upload-form-label-infoform-date": "Data",
"backend-fail-stream": "Non se puido transmitir o ficheiro \"$1\".",
"backend-fail-backup": "Non se puido facer unha copia de seguridade do ficheiro \"$1\".",
"backend-fail-notexists": "O ficheiro \"$1\" non existe.",
"nopagetext": "A páxina que especificou non existe.",
"pager-newer-n": "{{PLURAL:$1|unha posterior|$1 posteriores}}",
"pager-older-n": "{{PLURAL:$1|unha anterior|$1 anteriores}}",
- "suppress": "Supervisor",
+ "suppress": "Supresor",
"querypage-disabled": "Esta páxina especial está desactivada por razóns de rendemento.",
"apihelp": "Axuda coa API",
"apihelp-no-such-module": "Non se atopou o módulo \"$1\".",
"emailccsubject": "Copia da súa mensaxe para $1: $2",
"emailsent": "Mensaxe enviada",
"emailsenttext": "A súa mensaxe de correo electrónico foi enviada.",
- "emailuserfooter": "Este correo electrónico foi enviado por $1 a $2 mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
+ "emailuserfooter": "Este correo electrónico foi {{GENDER:$1|enviado}} por $1 a {{GENDER:$2|$2}} mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
"usermessage-summary": "Mensaxe deixada polo sistema.",
"usermessage-editor": "Editor das mensaxes do sistema",
"watchlist": "Lista de vixilancia",
"deletepage": "Borrar a páxina",
"confirm": "Confirmar",
"excontent": "o contido era: \"$1\"",
- "excontentauthor": "o contido era: \"$1\" (e o único editor foi [[Special:Contributions/$2|$2]])",
+ "excontentauthor": "o contido era: \"$1\", e o único editor foi \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|conversa]])",
"exbeforeblank": "o contido antes do baleirado era: \"$1\"",
"delete-confirm": "Borrar \"$1\"",
"delete-legend": "Borrar",
"move-page-legend": "Mover unha páxina",
"movepagetext": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nPode actualizar automaticamente as redireccións que van dar ao título orixinal.\nSe escolle non facelo, asegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
"movepagetext-noredirectfixer": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nAsegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
- "movepagetalktext": "A páxina de conversa asociada, se existe, será automaticamente movida con esta '''agás que''':\n*Estea a mover a páxina empregando espazos de nomes,\n*Xa exista unha páxina de conversa con ese nome, ou\n*Desactive a opción de abaixo.\n\nNestes casos, terá que mover ou mesturar a páxina manualmente se o desexa.",
+ "movepagetalktext": "Se marca esta caixa, a páxina de conversa asociada trasladarase automáticamente ó título novo a menos que xa exista unha páxina de conversa non baleira alí.\n\nNeste caso, deberá trasladar ou fusionar manualmente a páxina se así o quere.",
"moveuserpage-warning": "'''Aviso:''' Está a piques de mover unha páxina de usuario. Por favor, teña en conta que só se trasladará a páxina e que o usuario '''non''' será renomeado.",
"movecategorypage-warning": "'''Aviso:''' Está a piques de mover unha páxina de categoría. Por favor, teña en conta que só se trasladará a páxina e que as páxinas presentes na categoría vella '''non''' serán recategorizadas na categoría nova.",
"movenologintext": "Debe ser un usuario rexistrado e [[Special:UserLogin|acceder ao sistema]] para mover unha páxina.",
"cant-move-to-user-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de usuario (agás a unha subpáxina).",
"cant-move-category-page": "Non ten os permisos necesarios para mover páxinas de categoría.",
"cant-move-to-category-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de categoría.",
- "newtitle": "Ao novo título:",
+ "newtitle": "Novo título:",
"move-watch": "Vixiar esta páxina",
"movepagebtn": "Mover a páxina",
"pagemovedsub": "O movemento foi un éxito",
"logentry-newusers-byemail": "$1 {{GENDER:$2|creou}} a conta de usuario $3; o contrasinal enviouse por correo electrónico",
"logentry-newusers-autocreate": "A conta de {{GENDER:$2|usuario|usuaria}} $1 creouse automaticamente",
"logentry-protect-move_prot": "$1 {{GENDER:$2|trasladou}} a protección de \"$4\" a \"$3\"",
+ "logentry-protect-unprotect": "$1 {{GENDER:$2|eliminou}} a protección de $3",
+ "logentry-protect-protect": "$1 {{GENDER:$2|protexeu}} a $3 $4",
+ "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protexeu}} a $3 $4 [en cascada]",
+ "logentry-protect-modify": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4",
+ "logentry-protect-modify-cascade": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4 [en cascada]",
"logentry-rights-rights": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3 de $4 a $5",
"logentry-rights-rights-legacy": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3",
"logentry-rights-autopromote": "$1 foi {{GENDER:$2|promovido|promovida}} automaticamente de $4 a $5",
"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",
"undo-failure": "Dat kunnt mer nit zeröck nämme, dä Afschnedd wood enzwesche ald widder beärbeidt.",
"undo-norev": "Do kam_mer nix zeröck nämme. Di väsjohn jidd_et nit, udder se es verschtoche udder fottjeschmeße woode.",
"undo-nochange": "Di Änderong schingk ald retuur jemaat woode ze sin.",
- "undo-summary": "De Änderong $1 fum [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
+ "undo-summary": "Di Änderong $1 wood {{GENDER:$2|vum|vum|vumm Metmaacher|vun dä|vum}} [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
"undo-summary-username-hidden": "Nemm di Väsjohn $1 vun enem verschtoche Metmaacher widder retuhr.",
"cantcreateaccounttitle": "Kann keine Zojang enrichte",
"cantcreateaccount-text": "Dä [[User:$3|$3]] hät verbodde, dat mer sich vun dä IP-Adress '''$1''' uß als ene neue Metmaacher aanmelde könne soll.\n\nAls Jrund för et Sperre es enjedraare: ''$2''",
"recentchangeslinked-summary": "Heh di {{int:nstab-special}} hädd en Leß met Änderonge aan Sigge, di vun dä aanjejovve Sigg uß verlengk sin.\nBei Saachjroppe sen et de Sigge en dä Saachjropp.\nSigge uß Dinge [[Special:Watchlist|Opaßleß]] sin en '''Fättschreff''' jeschrevve.",
"recentchangeslinked-page": "Dä Sigg ier Övverschreff:",
"recentchangeslinked-to": "Zeisch de Änderonge aan dä Sigge, woh Lengks op di aanjejovve Sigg drop sin",
+ "autochange-username": "Automattesche Ännderong aam MediaWiki",
"upload": "Daate huhlade",
"uploadbtn": "Huhlade!",
"reuploaddesc": "Zeröck noh de Sigg zem Huhlade.",
"cant-move-to-user-page": "Do häs nit dat Rääsch, en Sigg tirkäk op en Metmaacher-Sigg ömzenänne, Do kanns se ävver op en Ungersigg dofun ömnenne.",
"cant-move-category-page": "Do häß nit dat Rääsch, Saachjroppesigge ömzebenänne.",
"cant-move-to-category-page": "Do häß nit dat Rääsch, en Sigg obb en Saachjroppesigg ömzebenänne.",
- "newtitle": "op dä neue Nahme",
+ "newtitle": "Dä neuje Nahme:",
"move-watch": "Op di Sigg heh oppaßße",
"movepagebtn": "Ömnenne",
"pagemovedsub": "Dat Ömnenne hät jeflupp",
"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",
"undeletethispage": "Ticmāquīxtīz inīn tlaīxtli",
"undelete_short": "Ahticpolōz {{PLURAL:$1|cē tlapatlaliztli|$1 tlapatlaliztli}}",
"viewdeleted_short": "Mà mỏta {{PLURAL:$1|se tlatlaìxpôpolòlli tlayèktlàlilistli|$1 tlatlaìxpôpolòltin tlayèktlàlilistin}}",
- "protect": "Ticquīxtīz",
+ "protect": "Ticpiyaz",
"protect_change": "ticpatlaz",
- "protectthispage": "Ticquīxtiāz inīn zāzanilli",
- "unprotect": "Ticpatlaz in tlaquīxtīliztli",
- "unprotectthispage": "Ticpatlaz inīn āmatl ītlaquīxtīliz",
+ "protectthispage": "Ticpiyaz inīn tlaīxtli",
+ "unprotect": "Ticpatlaz in tlapiyaliztli",
+ "unprotectthispage": "Ticpatlaz inīn tlaīxtli ītlapiyaliz",
"newpage": "Yancuic tlaīxtli",
"talkpage": "Tictlahtōz inīn zāzaniltechcopa",
"talkpagelinktext": "Zānīlli",
"badaccess": "Tlahuelītiliztechcopa ahcuallōtl",
"badaccess-group0": "Tehhuātl ahmo tiquichīhua inōn tiquiēlēhuia.",
"badaccess-groups": "Inōn tiquiēlēhuia zan quichīhuah tlatequitiltilīlli {{PLURAL:$2|oncān}}: $1.",
- "ok": "Nopan iti",
+ "ok": "Cualli",
"retrievedfrom": "Ōquīzqui ītech \"$1\"",
"youhavenewmessages": "Tiquimpiya $1 ($2).",
"youhavenewmessagesmulti": "Tiquimpiya yancuīc tlahcuilōlli īpan $1",
"editsection": "ticpatlaz",
"editold": "ticpatlaz",
- "viewsourceold": "xiquitta tlahtōlcaquiliztilōni",
+ "viewsourceold": "tiquittaz mēyalli",
"editlink": "ticpatlaz",
- "viewsourcelink": "tiquittaz tlahtōlcaquiliztilōni",
+ "viewsourcelink": "tiquittaz mēyalli",
"editsectionhint": "Ticpatlacah: $1",
"toc": "Inīn tlahcuilōlco",
- "showtoc": "xiquitta",
+ "showtoc": "ticnēxtīz",
"hidetoc": "tictlātīz",
"collapsible-collapse": "Motlàtìs",
"collapsible-expand": "Monèxtìs",
"nstab-media": "Mēdiatl",
"nstab-special": "Nònkuâkìskàtlaìxtlapalli",
"nstab-project": "Ìtlaìxtlapal in tlayẻkàntekitl",
- "nstab-image": "Īxiptli",
+ "nstab-image": "Ihcuilōlli",
"nstab-mediawiki": "Tlahcuilōltzintli",
"nstab-template": "Nemachiòtl",
"nstab-help": "Tèpalèwilistli",
"badtitle": "Ahcualli tōcāitl",
"badtitletext": "Zāzanilli ticnequi in ītōca cah ahcualli, ahtlein quipiya nozo ahcualtzonhuiliztli interwiki tōcāhuicpa.\nHueliz quimpiya tlahtōl tlein ahmo mohuelītih motequitiltia tōcāpan.",
"viewsource": "Tiquittaz tlahtōlcaquiliztilōni",
+ "viewsource-title": "Tiquittaz $1 īmēyal",
"actionthrottled": "Tlachīhualiztli ōmotzacuili",
"viewsourcetext": "Tihuelīti tiquitta auh ticcopīna inīn zāzanilli ītlahtōlcaquiliztilōni:",
"namespaceprotected": "Ahmo tiquihuelīti tiquimpatla zāzaniltin īpan '''$1'''.",
"logout": "Tiquīzaz",
"userlogout": "Tiquīzaz",
"notloggedin": "Ahmō ōtimocalac",
- "nologin": "¿Ahmō ticpiya cuentah? '''$1'''.",
+ "userlogin-noaccount": "Cuix ahmō titlapōhualeh?",
+ "nologin": "Cuix ahmō titlapōhualeh? $1.",
"nologinlink": "Ticchīhuaz cē cuentah",
"createaccount": "Ticchīhuaz cuentah",
"gotaccount": "¿Ye ticpiya cē tlapōhualli? '''$1'''.",
"powersearch-toggleall": "Mochi",
"powersearch-togglenone": "Ahtlein",
"search-external": "Tlatēmotiliztli calāmpa",
- "preferences": "Tlaēlēhuiliztli",
+ "preferences": "Panitlatlālīlli",
"mypreferences": "Notlaēlēhuiliz",
"prefs-edits": "Tlapatlaliztli tlapōhualli:",
"prefs-skin": "Ēhuatl",
"right-block": "Tiquintzacuilīz occequīntīn tlatequitiltilīlli",
"right-blockemail": "Titēquīxtīz tlatequitiltilīlli ic tēch-e-mailīz",
"right-hideuser": "Ticquīxtīz cē tlatequitiltilīltōcāitl, āuh ichtac",
+ "right-editmyoptions": "Ticpatlaz mopanitlatlālīl",
"right-import": "Ticcōhuāz zāzaniltin occequīntīn huiquihuīcpa",
"right-importupload": "Tiquincōhuāz zāzaniltin tlahcuilōlquetzalizhuīcpa",
"right-patrolmarks": "Tiquinttāz tlapiyalizmachiyōtl īpan yancuīc tlapatlaliztli",
"sp-contributions-newbies-title": "Yancuīc tlatequitiltilīlli ītlahcuilōl",
"sp-contributions-blocklog": "Tlatzacuiliztli tlahcuilōlloh",
"sp-contributions-uploads": "tlahcuilōlquetzaliztli",
- "sp-contributions-talk": "tēixnāmiquiliztli",
+ "sp-contributions-talk": "zānīlli",
"sp-contributions-search": "Tiquintlatēmōz tlapatlaliztli",
"sp-contributions-username": "IP nozo tlatequitiltilīlli ītōcā:",
"sp-contributions-submit": "Tlatēmōz",
"import-upload": "Tiquinquetzāz XML tlahcuilōlli",
"importlogpage": "Tiquincōhuāz tlahcuilōlloh",
"tooltip-pt-userpage": "Notlatequitiltilīlzāzanil",
- "tooltip-pt-mytalk": "Notēixnāmiquiliz",
+ "tooltip-pt-mytalk": "Mozānīl",
"tooltip-pt-preferences": "Mopanitlatlālīl",
"tooltip-pt-watchlist": "Zāzaniltin tiquintlachiya ic tlapatlaliztli",
"tooltip-pt-mycontris": "Notlahcuilōl",
"recentchangeslinked-summary": "Deze speciale pagina geeft de laatste bewerkingen weer op pagina's waarheen verwezen wordt vanaf een opgegeven pagina of op pagina's in een opgegeven categorie.\nPagina's die op [[Special:Watchlist|uw volglijst]] staan worden '''vet''' weergegeven.",
"recentchangeslinked-page": "Paginanaam:",
"recentchangeslinked-to": "Wijzigingen aan pagina's met koppelingen naar deze pagina bekijken",
+ "recentchanges-page-added-to-category": "[[:$1]] aan categorie toegevoegd",
"upload": "Bestand uploaden",
"uploadbtn": "Bestand uploaden",
"reuploaddesc": "Upload annuleren en terugkeren naar het uploadformulier",
"htmlform-cloner-create": "Meer toevoegen",
"htmlform-cloner-delete": "Verwijderen",
"htmlform-cloner-required": "Ten minste één waarde is vereist.",
+ "htmlform-title-not-exists": "[[:$1]] bestaat niet.",
+ "htmlform-user-not-exists": "<strong>$1</strong> bestaat niet.",
+ "htmlform-user-not-valid": "<strong>$1</strong> is geen geldige gebruikersnaam.",
"sqlite-has-fts": "Versie $1 met ondersteuning voor \"full-text\" zoeken",
"sqlite-no-fts": "Versie $1 zonder ondersteuning voor \"full-text\" zoeken",
"logentry-delete-delete": "$1 {{GENDER:$2|heeft}} de pagina $3 verwijderd",
"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.",
"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",
"watchlistanontext": "Пожалуйста, войдите, чтобы просмотреть или отредактировать элементы в списке наблюдения.",
"watchnologin": "Нужно представиться системе",
"addwatch": "Добавить в список наблюдения",
- "addedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
+ "addedwatchtext": "Страница «[[:$1]]» вместе с её обсуждением были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
"addedwatchtext-short": "Страница «$1» была добавлена в ваш список наблюдения.",
"removewatch": "Удалить из списка наблюдения",
"removedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были удалены из вашего [[Special:Watchlist|списка наблюдения]].",
"exbeforeblank": "содержимое до очистки: «$1»",
"delete-confirm": "$1 — удаление",
"delete-legend": "Удаление",
- "historywarning": "<strong>Ð\92нимание:</strong> У Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b, коÑ\82оÑ\80Ñ\83Ñ\8e вÑ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c, еÑ\81Ñ\82Ñ\8c иÑ\81Ñ\82оÑ\80иÑ\8f пÑ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|версий}}:",
+ "historywarning": "<strong>Ð\92нимание:</strong> Ð\92Ñ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ñ\83 коÑ\82оÑ\80ой еÑ\81Ñ\82Ñ\8c иÑ\81Ñ\82оÑ\80иÑ\8f пÑ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|веÑ\80Ñ\81ии|версий}}:",
"confirmdeletetext": "Вы запросили полное удаление страницы (или изображения) и всей её истории изменений. Пожалуйста, подтвердите, что вы действительно желаете это сделать, понимаете последствия своих действий, и делаете это в соответствии [[{{MediaWiki:Policy-url}}|с правилами]].",
"actioncomplete": "Действие выполнено",
"actionfailed": "Действие не выполнено",
"undeletepagetext": "{{PLURAL:$1|Следующая $1 страница была удалена|Следующие $1 страниц были удалены|Следующие $1 страницы были удалены|1=Следующая страница была удалена}}, однако {{PLURAL:$1|1=она всё ещё находится в архиве и поэтому может быть восстановлена|они всё ещё находятся в архиве и поэтому могут быть восстановлены}}.\nАрхив может периодически очищаться.",
"undelete-fieldset-title": "Восстановить версии",
"undeleteextrahelp": "Для полного восстановления истории страницы оставьте все отметки пустыми и нажмите '''«{{int:undeletebtn}}»'''.\nДля частичного восстановления отметьте те версии страницы, которые нужно восстановить, и нажмите '''«{{int:undeletebtn}}»'''.",
- "undeleterevisions": "$1 {{PLURAL:$1|версия|версий|версии}} {{PLURAL:$1|удалена|удалены}}",
+ "undeleterevisions": "$1 {{PLURAL:$1|удалённая версия|удалённые версии|удалённых версий}}",
"undeletehistory": "При восстановлении страницы восстанавливается и её история правок.\nЕсли после удаления была создана новая страница с тем же названием, то восстановленные версии появятся в истории правок перед новыми версиями.",
"undeleterevdel": "Восстановление не будет произведено, если оно приведёт к частичному удалению последней версии страницы или файла.\nВ подобном случае вы должны снять отметку или показать последние удалённые версии.",
"undeletehistorynoadmin": "Статья была удалена. Причина удаления и список участников, редактировавших статью до её удаления, показаны ниже. Текст удалённой статьи могут просмотреть только администраторы.",
$batch = array();
$lastName = '';
do {
- $res = $dbw->select( 'image', array( 'img_name', 'img_sha1' ),
+ $res = $dbw->select( 'image',
+ array( 'img_name', 'img_sha1' ),
array_merge( array( 'img_name > ' . $dbw->addQuotes( $lastName ) ), $conds ),
__METHOD__,
array( 'LIMIT' => $this->mBatchSize, 'ORDER BY' => 'img_name' )
foreach ( $res as $row ) {
$lastName = $row->img_name;
- $sha1 = $row->img_sha1;
+ /** @var LocalFile $file */
+ $file = $repo->newFile( $row->img_name );
+ // Check in case SHA1 rows are not populated for some files
+ $sha1 = strlen( $row->img_sha1 ) ? $row->img_sha1 : $file->getSha1();
+
if ( !strlen( $sha1 ) ) {
- $this->error( "Image SHA-1 not set for {$row->img_name}." );
+ $this->error( "Image SHA-1 not known for {$row->img_name}." );
} else {
- $file = $repo->newFile( $row->img_name );
-
if ( $oldLayout === 'sha1' ) {
$spath = "{$origBase}/{$sha1[0]}/{$sha1[1]}/{$sha1[2]}/{$sha1}";
} else {
$dpath = $file->getPath();
}
- $status = $be->prepare( array( 'dir' => dirname( $dpath ) ) );
+ $status = $be->prepare( array(
+ 'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ) );
if ( !$status->isOK() ) {
$this->error( print_r( $status->getErrorsArray(), true ) );
}
$dpath = $ofile->getPath();
}
- $status = $be->prepare( array( 'dir' => dirname( $dpath ) ) );
+ $status = $be->prepare( array(
+ 'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ) );
if ( !$status->isOK() ) {
$this->error( print_r( $status->getErrorsArray(), true ) );
}
'/' . $repo->getDeletedHashPath( $sha1Key ) . $sha1Key;
}
- $status = $be->prepare( array( 'dir' => dirname( $dpath ) ) );
+ $status = $be->prepare( array(
+ 'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ) );
if ( !$status->isOK() ) {
$this->error( print_r( $status->getErrorsArray(), true ) );
}
$this->output( "\"{$op['img']}\" (dest: {$op['dst']})\n" );
}
- $status = $be->doOperations( $ops );
+ $status = $be->doOperations( $ops, array( 'bypassReadOnly' => 1 ) );
if ( !$status->isOK() ) {
$this->output( print_r( $status->getErrorsArray(), true ) );
}
<ruleset name="MediaWiki">
<rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
<!-- Disable failing rules -->
- <exclude name="Generic.Files.LineLength"/>
- <exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
<exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.EmptyComment"/>
</rule>
<rule ref="Generic.Files.LineLength">
<exclude-pattern>*/languages/messages/Messages*.php</exclude-pattern>
</rule>
+ <rule ref="PSR2.Methods.MethodDeclaration.Underscore">
+ <exclude-pattern>*/includes/StubObject.php</exclude-pattern>
+ </rule>
<exclude-pattern>node_modules</exclude-pattern>
<exclude-pattern>vendor</exclude-pattern>
<exclude-pattern>extensions</exclude-pattern>
'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' )
),
),
'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' )
),
),
*/
mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
var fieldset,
- target = mw.config.get( 'wgRemoteUploadTarget' ),
+ targets = mw.config.get( 'wgForeignUploadTargets' ),
+ // Default to using local, but try to use a configured target.
+ // TODO allow finer configuration of this somehow?
+ target = ( targets && targets.length ) ? targets[ 0 ] : 'local',
$ownWorkMessage = $( '<p>' ).html(
mw.message( 'foreign-structured-upload-form-label-own-work-message-' + target ).parse()
),
-( function ( mw, OO ) {
+( function ( mw, OO, $ ) {
/**
* @class mw.ForeignUpload
* @extends mw.Upload
* instead.
*
* @constructor
- * @param {string} [targetHost="commons.wikimedia.org"] Used to set up the target
+ * @param {string} [target="local"] Used to set up the target
* wiki. If not remote, this class behaves identically to mw.Upload (unless further subclassed)
+ * Use the same names as set in $wgForeignFileRepos for this. Also,
+ * make sure there is an entry in the $wgForeignUploadTargets array
+ * set to "true" for this name.
* @param {Object} [apiconfig] Passed to the constructor of mw.ForeignApi or mw.Api, as needed.
*/
- function ForeignUpload( targetHost, apiconfig ) {
- var api;
+ function ForeignUpload( target, apiconfig ) {
+ var api, upload = this;
- if ( typeof targetHost === 'object' ) {
- // targetHost probably wasn't passed in, it must
+ if ( typeof target === 'object' ) {
+ // target probably wasn't passed in, it must
// be apiconfig
- apiconfig = targetHost;
- } else {
- // targetHost is a useful string, set it here
- this.targetHost = targetHost || this.targetHost;
+ apiconfig = target;
+ target = undefined;
}
- if ( location.host !== this.targetHost ) {
- api = new mw.ForeignApi(
- location.protocol + '//' + this.targetHost + '/w/api.php',
- apiconfig
- );
+ // Resolve defaults etc. - if target isn't passed in, we use
+ // the default.
+ this.target = target || this.target;
+
+ // Now we have several different options.
+ // If the local wiki is the target, then we can skip a bunch of steps
+ // and just return an mw.Api object, because we don't need any special
+ // configuration for that.
+ // However, if the target is a remote wiki, we must check the API
+ // to confirm that the target is one that this site is configured to
+ // support.
+ if ( this.target !== 'local' ) {
+ api = new mw.Api();
+ this.apiPromise = api.get( {
+ action: 'query',
+ meta: 'filerepoinfo',
+ friprop: [ 'name', 'scriptDirUrl', 'canUpload' ]
+ } ).then( function ( data ) {
+ var i, repo,
+ repos = data.query.repos;
+
+ for ( i in repos ) {
+ repo = repos[ i ];
+
+ if ( repo.name === upload.target ) {
+ // This is our target repo.
+ if ( !repo.canUpload ) {
+ // But it's not configured correctly.
+ return $.Deferred().reject( 'repo-cannot-upload' );
+ }
+
+ return new mw.ForeignApi(
+ repo.scriptDirUrl + '/api.php',
+ apiconfig
+ );
+ }
+ }
+ } );
} else {
- // We'll ignore the CORS and centralauth stuff if we're on Commons already
- api = new mw.Api( apiconfig );
+ // We'll ignore the CORS and centralauth stuff if the target is
+ // the local wiki.
+ this.apiPromise = $.Deferred().resolve( new mw.Api( apiconfig ) );
}
- mw.Upload.call( this, api );
+ // Build the upload object without an API - this class overrides the
+ // actual API call methods to wait for the apiPromise to resolve
+ // before continuing.
+ mw.Upload.call( this, null );
}
OO.inheritClass( ForeignUpload, mw.Upload );
* @property targetHost
* Used to specify the target repository of the upload.
*
- * You could override this to point at something that isn't Commons,
- * but be sure it has the correct templates and is CORS and CentralAuth
- * ready.
+ * If you set this to something that isn't 'local', you must be sure to
+ * add that target to $wgForeignUploadTargets in LocalSettings, and the
+ * repository must be set up to use CORS and CentralAuth.
+ */
+ ForeignUpload.prototype.target = 'local';
+
+ /**
+ * Override from mw.Upload to make sure the API info is found and allowed
+ */
+ ForeignUpload.prototype.upload = function () {
+ var upload = this;
+ return this.apiPromise.then( function ( api ) {
+ upload.api = api;
+ return mw.Upload.prototype.upload.call( upload );
+ } );
+ };
+
+ /**
+ * Override from mw.Upload to make sure the API info is found and allowed
*/
- ForeignUpload.prototype.targetHost = 'commons.wikimedia.org';
+ ForeignUpload.prototype.uploadToStash = function () {
+ var upload = this;
+ return this.apiPromise.then( function ( api ) {
+ upload.api = api;
+ return mw.Upload.prototype.uploadToStash.call( upload );
+ } );
+ };
mw.ForeignUpload = ForeignUpload;
-}( mediaWiki, OO ) );
+}( mediaWiki, OO, jQuery ) );
// Caching is somewhat problematic, because we do need different message functions for different maps, so
// we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
// Do not use mw.jqueryMsg unless required
- if ( this.format === 'plain' || !/\{\{|[\[<>]/.test( this.map.get( this.key ) ) ) {
+ if ( this.format === 'plain' || !/\{\{|[\[<>&]/.test( this.map.get( this.key ) ) ) {
// Fall back to mw.msg's simple parser
return oldParser.apply( this );
}
$this->assertRegExp( '/<li class="[\w\s-]*mw-tag-newbie[\w\s-]*">/', $line );
}
+ public function testRecentChangesLine_numberOfWatchingUsers() {
+ $oldChangesList = $this->getOldChangesList();
+
+ $recentChange = $this->getEditChange();
+ $recentChange->numberofWatchingusers = 100;
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/(number_of_watching_users_RCview: 100)/", $line );
+ }
+
+ public function testRecentChangesLine_watchlistCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $oldChangesList->setWatchlistDivs( true );
+
+ $recentChange = $this->getEditChange();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/watchlist-0-Cat/", $line );
+ }
+
private function getNewBotEditChange() {
$user = $this->getTestUser();
--- /dev/null
+<?php
+/**
+ * A MemoizedCallable subclass that stores function return values
+ * in an instance property rather than APC.
+ */
+class ArrayBackedMemoizedCallable extends MemoizedCallable {
+ public $cache = array();
+
+ protected function fetchResult( $key, &$success ) {
+ if ( array_key_exists( $key, $this->cache ) ) {
+ $success = true;
+ return $this->cache[$key];
+ }
+ $success = false;
+ return false;
+ }
+
+ protected function storeResult( $key, $result ) {
+ $this->cache[$key] = $result;
+ }
+}
+
+
+/**
+ * PHP Unit tests for MemoizedCallable class.
+ * @covers MemoizedCallable
+ */
+class MemoizedCallableTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * The memoized callable should relate inputs to outputs in the same
+ * way as the original underlying callable.
+ */
+ public function testReturnValuePassedThrough() {
+ $mock = $this->getMock( 'stdClass', array( 'reverse' ) );
+ $mock->expects( $this->any() )
+ ->method( 'reverse' )
+ ->will( $this->returnCallback( 'strrev' ) );
+
+ $memoized = new MemoizedCallable( array( $mock, 'reverse' ) );
+ $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
+ }
+
+ /**
+ * Consecutive calls to the memoized callable with the same arguments
+ * should result in just one invocation of the underlying callable.
+ *
+ * @requires function apc_store
+ */
+ public function testCallableMemoized() {
+ $observer = $this->getMock( 'stdClass', array( 'computeSomething' ) );
+ $observer->expects( $this->once() )
+ ->method( 'computeSomething' )
+ ->will( $this->returnValue( 'ok' ) );
+
+ $memoized = new ArrayBackedMemoizedCallable( array( $observer, 'computeSomething' ) );
+
+ // First invocation -- delegates to $observer->computeSomething()
+ $this->assertEquals( 'ok', $memoized->invoke() );
+
+ // Second invocation -- returns memoized result
+ $this->assertEquals( 'ok', $memoized->invoke() );
+ }
+
+ /**
+ * @covers MemoizedCallable::invoke
+ */
+ public function testInvokeVariadic() {
+ $memoized = new MemoizedCallable( 'sprintf' );
+ $this->assertEquals(
+ $memoized->invokeArgs( array( 'this is %s', 'correct' ) ),
+ $memoized->invoke( 'this is %s', 'correct' )
+ );
+ }
+
+ /**
+ * @covers MemoizedCallable::call
+ */
+ public function testShortcutMethod() {
+ $this->assertEquals(
+ 'this is correct',
+ MemoizedCallable::call( 'sprintf', array( 'this is %s', 'correct' ) )
+ );
+ }
+
+ /**
+ * Outlier TTL values should be coerced to range 1 - 86400.
+ */
+ public function testTTLMaxMin() {
+ $memoized = new MemoizedCallable( 'abs', 100000 );
+ $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
+
+ $memoized = new MemoizedCallable( 'abs', -10 );
+ $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
+ }
+
+ /**
+ * Closure names should be distinct.
+ */
+ public function testMemoizedClosure() {
+ $a = new MemoizedCallable( function () {
+ return 'a';
+ } );
+
+ $b = new MemoizedCallable( function () {
+ return 'b';
+ } );
+
+ $this->assertEquals( $a->invokeArgs(), 'a' );
+ $this->assertEquals( $b->invokeArgs(), 'b' );
+
+ $this->assertNotEquals(
+ $this->readAttribute( $a, 'callableName' ),
+ $this->readAttribute( $b, 'callableName' )
+ );
+ }
+
+ /**
+ * @expectedExceptionMessage non-scalar argument
+ * @expectedException InvalidArgumentException
+ */
+ public function testNonScalarArguments() {
+ $memoized = new MemoizedCallable( 'gettype' );
+ $memoized->invoke( new stdClass() );
+ }
+
+ /**
+ * @expectedExceptionMessage must be an instance of callable
+ * @expectedException InvalidArgumentException
+ */
+ public function testNotCallable() {
+ $memoized = new MemoizedCallable( 14 );
+ }
+}
$this->assertEquals( $this->cache->get( $key ), $value );
}
+ /**
+ * @covers BagOStuff::getWithSetCallback
+ */
+ public function testGetWithSetCallback() {
+ $key = wfMemcKey( 'test' );
+ $value = $this->cache->getWithSetCallback(
+ $key,
+ 30,
+ function () {
+ return 'hello kitty';
+ }
+ );
+
+ $this->assertEquals( 'hello kitty', $value );
+ $this->assertEquals( $value, $this->cache->get( $key ) );
+ }
+
/**
* @covers BagOStuff::incr
*/
protected function mockDb() {
// Cant mock from DatabaseType or DatabaseBase, they dont
// have the full gamut of methods
+ // FIXME: the constructor normally sets mAtomicLevels and mSrvCache
$databaseMysql = $this->getMockBuilder( 'DatabaseMysql' )
->disableOriginalConstructor()
->getMock();
$databaseMysql->expects( $this->any() )
->method( 'isOpen' )
->will( $this->returnValue( true ) );
+ $databaseMysql->expects( $this->any() )
+ ->method( 'getApproximateLagStatus' )
+ ->will( $this->returnValue( array( 'lag' => 0, 'since' => 0 ) ) );
return $databaseMysql;
}
}
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 ) );
} );
// HTML in wikitext
- QUnit.test( 'HTML', 26, function ( assert ) {
+ QUnit.test( 'HTML', 32, function ( assert ) {
mw.messages.set( 'jquerymsg-italics-msg', '<i>Very</i> important' );
assertBothModes( assert, [ 'jquerymsg-italics-msg' ], mw.messages.get( 'jquerymsg-italics-msg' ), 'Simple italics unchanged' );
'Foo<tag/>bar',
'Self-closing tags don\'t cause a parse error'
);
+
+ mw.messages.set( 'jquerymsg-entities1', 'A&B' );
+ mw.messages.set( 'jquerymsg-entities2', 'A>B' );
+ mw.messages.set( 'jquerymsg-entities3', 'A→B' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities1' ),
+ 'A&B',
+ 'Lone "&" is escaped in text'
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities2' ),
+ 'A&gt;B',
+ '">" entity is double-escaped in text' // (WHY?)
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities3' ),
+ 'A&rarr;B',
+ '"→" entity is double-escaped in text'
+ );
+
+ mw.messages.set( 'jquerymsg-entities-attr1', '<i title="A&B"></i>' );
+ mw.messages.set( 'jquerymsg-entities-attr2', '<i title="A>B"></i>' );
+ mw.messages.set( 'jquerymsg-entities-attr3', '<i title="A→B"></i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities-attr1' ),
+ '<i title="A&B"></i>',
+ 'Lone "&" is escaped in attribute'
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities-attr2' ),
+ '<i title="A>B"></i>',
+ '">" entity is not double-escaped in attribute' // (WHY?)
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-entities-attr3' ),
+ '<i title="A&rarr;B"></i>',
+ '"→" entity is double-escaped in attribute'
+ );
} );
QUnit.test( 'Behavior in case of invalid wikitext', 3, function ( assert ) {