'CleanupUsersWithNoId' => __DIR__ . '/maintenance/cleanupUsersWithNoId.php',
'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php',
'ClearUserWatchlistJob' => __DIR__ . '/includes/jobqueue/jobs/ClearUserWatchlistJob.php',
+ 'ClearWatchlistNotificationsJob' => __DIR__ . '/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php',
'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php',
'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php',
'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc',
'categoryMembershipChange' => CategoryMembershipChangeJob::class,
'clearUserWatchlist' => ClearUserWatchlistJob::class,
'cdnPurge' => CdnPurgeJob::class,
- 'enqueue' => EnqueueJob::class, // local queue for multi-DC setups
'userGroupExpiry' => UserGroupExpiryJob::class,
+ 'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class,
+ 'enqueue' => EnqueueJob::class, // local queue for multi-DC setups
'null' => NullJob::class,
];
<?php
namespace MediaWiki;
+use ActorMigration;
use CommentStore;
use Config;
use ConfigFactory;
$userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
global $wgActorTableSchemaMigrationStage;
- $from = $fromName = false;
+ $fromName = false;
if ( !is_null( $this->params['continue'] ) ) {
$continue = explode( '|', $this->params['continue'] );
$this->dieContinueUsageIf( count( $continue ) != 4 );
$this->dieContinueUsageIf( $continue[0] !== 'name' );
$fromName = $continue[1];
- $from = "$op= " . $dbSecondary->addQuotes( $fromName );
}
$like = $dbSecondary->buildLike( $this->params['userprefix'], $dbSecondary->anyString() );
$limit = 501;
do {
+ $from = $fromName ? "$op= " . $dbSecondary->addQuotes( $fromName ) : false;
+
// For the new schema, pull from the actor table. For the
// old, pull from rev_user. For migration a FULL [OUTER]
// JOIN would be what we want, except MySQL doesn't support
}
$count = 0;
- $from = null;
+ $fromName = false;
foreach ( $res as $row ) {
if ( ++$count >= $limit ) {
- $from = $row->user_name;
+ $fromName = $row->user_name;
break;
}
yield User::newFromRow( $row );
}
- } while ( $from !== null );
+ } while ( $fromName !== false );
} );
// Do the actual sorting client-side, because otherwise
// prepareQuery might try to sort by actor and confuse everything.
/**
* Job for updating user activity like "last viewed" timestamps
*
+ * Job parameters include:
+ * - type: one of (updateWatchlistNotification) [required]
+ * - userid: affected user ID [required]
+ * - notifTime: timestamp to set watchlist entries to [required]
+ * - curTime: UNIX timestamp of the event that triggered this job [required]
+ *
* @ingroup JobQueue
* @since 1.26
*/
function __construct( Title $title, array $params ) {
parent::__construct( 'activityUpdateJob', $title, $params );
- if ( !isset( $params['type'] ) ) {
- throw new InvalidArgumentException( "Missing 'type' parameter." );
+ static $required = [ 'type', 'userid', 'notifTime', 'curTime' ];
+ $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
+ if ( $missing != '' ) {
+ throw new InvalidArgumentException( "Missing paramter(s) $missing" );
}
$this->removeDuplicates = true;
if ( $this->params['type'] === 'updateWatchlistNotification' ) {
$this->updateWatchlistNotification();
} else {
- throw new InvalidArgumentException(
- "Invalid 'type' parameter '{$this->params['type']}'." );
+ throw new InvalidArgumentException( "Invalid 'type' '{$this->params['type']}'." );
}
return true;
--- /dev/null
+<?php
+/**
+ * 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
+ * @ingroup JobQueue
+ */
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Job for clearing all of the "last viewed" timestamps for a user's watchlist
+ *
+ * Job parameters include:
+ * - userId: affected user ID [required]
+ * - casTime: UNIX timestamp of the event that triggered this job [required]
+ *
+ * @ingroup JobQueue
+ * @since 1.31
+ */
+class ClearWatchlistNotificationsJob extends Job {
+ function __construct( Title $title, array $params ) {
+ parent::__construct( 'clearWatchlistNotifications', $title, $params );
+
+ static $required = [ 'userId', 'casTime' ];
+ $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
+ if ( $missing != '' ) {
+ throw new InvalidArgumentException( "Missing paramter(s) $missing" );
+ }
+
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ $services = MediaWikiServices::getInstance();
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ $rowsPerQuery = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+
+ $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+
+ $asOfTimes = array_unique( $dbw->selectFieldValues(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [ 'wl_user' => $this->params['userId'], 'wl_notificationtimestamp IS NOT NULL' ],
+ __METHOD__,
+ [ 'ORDER BY' => 'wl_notificationtimestamp DESC' ]
+ ) );
+
+ foreach ( array_chunk( $asOfTimes, $rowsPerQuery ) as $asOfTimeBatch ) {
+ $dbw->update(
+ 'watchlist',
+ [ 'wl_notificationtimestamp' => null ],
+ [
+ 'wl_user' => $this->params['userId'],
+ 'wl_notificationtimestamp' => $asOfTimeBatch,
+ // New notifications since the reset should not be cleared
+ 'wl_notificationtimestamp < ' .
+ $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) )
+ ],
+ __METHOD__
+ );
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ }
+}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function fieldInfo( $table, $field ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function affectedRows() {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function reportConnectionError( $error = 'Unknown error' ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function freeResult( $res ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function indexUnique( $table, $index ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function listTables( $prefix = null, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function timestamp( $ts = 0 ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $res;
}
- /**
- * Turns on (false) or off (true) the automatic generation and sending
- * of a "we're sorry, but there has been a database error" page on
- * database errors. Default is on (false). When turned off, the
- * code should use lastErrno() and lastError() to handle the
- * situation as appropriate.
- *
- * Do not use this function outside of the Database classes.
- *
- * @param null|bool $ignoreErrors
- * @return bool The previous value of the flag.
- */
- protected function ignoreErrors( $ignoreErrors = null ) {
- $res = $this->getFlag( self::DBO_IGNORE );
- if ( $ignoreErrors !== null ) {
- // setFlag()/clearFlag() do not allow DBO_IGNORE changes for sanity
- if ( $ignoreErrors ) {
- $this->flags |= self::DBO_IGNORE;
- } else {
- $this->flags &= ~self::DBO_IGNORE;
- }
- }
-
- return $res;
- }
-
public function trxLevel() {
return $this->trxLevel;
}
*/
abstract protected function closeConnection();
+ /**
+ * @param string $error Fallback error message, used if none is given by DB
+ * @throws DBConnectionError
+ */
public function reportConnectionError( $error = 'Unknown error' ) {
$myError = $this->lastError();
if ( $myError ) {
return false;
}
+ /**
+ * Report a query error. Log the error, and if neither the object ignore
+ * flag nor the $tempIgnore flag is set, throw a DBQueryError.
+ *
+ * @param string $error
+ * @param int $errno
+ * @param string $sql
+ * @param string $fname
+ * @param bool $tempIgnore
+ * @throws DBQueryError
+ */
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $this->ignoreErrors() || $tempIgnore ) {
+ if ( $this->getFlag( self::DBO_IGNORE ) || $tempIgnore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
/** @var stdClass|null */
private $replicationInfoRow = null;
+ // Cache getServerId() for 24 hours
+ const SERVER_ID_CACHE_TTL = 86400;
+
/**
* Additional $params include:
* - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
}
// Wait on the GTID set (MariaDB only)
$gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
- $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ if ( strpos( $gtidArg, ':' ) !== false ) {
+ // MySQL GTIDs, e.g "source_id:transaction_id"
+ $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
+ } else {
+ // MariaDB GTIDs, e.g."domain:server:sequence"
+ $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ }
} else {
// Wait on the binlog coordinates
$encFile = $this->addQuotes( $pos->getLogFile() );
- $encPos = intval( $pos->pos[1] );
+ $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
}
$row = $res ? $this->fetchRow( $res ) : false;
if ( !$row ) {
- throw new DBExpectedError( $this,
- "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
+ throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
}
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
* @return MySQLMasterPos|bool
*/
public function getReplicaPos() {
- $now = microtime( true );
-
- if ( $this->useGTIDs ) {
- $res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- if ( $gtidRow && strlen( $gtidRow->Value ) ) {
- return new MySQLMasterPos( $gtidRow->Value, $now );
+ $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+ if ( $this->useGTIDs() ) {
+ // Try to use GTIDs, fallbacking to binlog positions if not possible
+ $data = $this->getServerGTIDs( __METHOD__ );
+ // Use gtid_current_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ return new MySQLMasterPos( $data[$name], $now );
+ }
}
}
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
- if ( $row && strlen( $row->Relay_Master_Log_File ) ) {
+ $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
+ if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
return new MySQLMasterPos(
- "{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}",
+ "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
$now
);
}
* @return MySQLMasterPos|bool
*/
public function getMasterPos() {
- $now = microtime( true );
+ $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+ $pos = false;
+ if ( $this->useGTIDs() ) {
+ // Try to use GTIDs, fallbacking to binlog positions if not possible
+ $data = $this->getServerGTIDs( __METHOD__ );
+ // Use gtid_current_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ $pos = new MySQLMasterPos( $data[$name], $now );
+ break;
+ }
+ }
+ // Filter domains that are inactive or not relevant to the session
+ if ( $pos ) {
+ $pos->setActiveOriginServerId( $this->getServerId() );
+ $pos->setActiveOriginServerUUID( $this->getServerUUID() );
+ if ( isset( $data['gtid_domain_id'] ) ) {
+ $pos->setActiveDomain( $data['gtid_domain_id'] );
+ }
+ }
+ }
- if ( $this->useGTIDs ) {
- $res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- if ( $gtidRow && strlen( $gtidRow->Value ) ) {
- return new MySQLMasterPos( $gtidRow->Value, $now );
+ if ( !$pos ) {
+ $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
+ if ( $data && strlen( $data['File'] ) ) {
+ $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
}
}
- $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
- if ( $row && strlen( $row->File ) ) {
- return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now );
+ return $pos;
+ }
+
+ /**
+ * @return int
+ * @throws DBQueryError If the variable doesn't exist for some reason
+ */
+ protected function getServerId() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SELECT @@server_id AS id", __METHOD__ );
+ return intval( $this->fetchObject( $res )->id );
+ }
+ );
+ }
+
+ /**
+ * @return string|null
+ */
+ protected function getServerUUID() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" );
+ $row = $this->fetchObject( $res );
+
+ return $row ? $row->Value : null;
+ }
+ );
+ }
+
+ /**
+ * @param string $fname
+ * @return string[]
+ */
+ protected function getServerGTIDs( $fname = __METHOD__ ) {
+ $map = [];
+ // Get global-only variables like gtid_executed
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname );
+ foreach ( $res as $row ) {
+ $map[$row->Variable_name] = $row->Value;
+ }
+ // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
+ $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname );
+ foreach ( $res as $row ) {
+ $map[$row->Variable_name] = $row->Value;
}
- return false;
+ return $map;
+ }
+
+ /**
+ * @param string $role One of "MASTER"/"SLAVE"
+ * @param string $fname
+ * @return string[] Latest available server status row
+ */
+ protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
+ return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: [];
}
public function serverIsReadOnly() {
return 'CAST( ' . $field . ' AS SIGNED )';
}
+ /*
+ * @return bool Whether GTID support is used (mockable for testing)
+ */
+ protected function useGTIDs() {
+ return $this->useGTIDs;
+ }
}
class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
* Should return true if unsure.
*
* @return bool
+ * @deprecated Since 1.31; use lastDoneWrites()
*/
public function doneWrites();
*/
public function lastError();
- /**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param string $table Table name
- * @param string $field Field name
- *
- * @return Field
- */
- public function fieldInfo( $table, $field );
-
/**
* Get the number of rows affected by the last write query
* @see https://secure.php.net/mysql_affected_rows
*/
public function close();
- /**
- * @param string $error Fallback error message, used if none is given by DB
- * @throws DBConnectionError
- */
- public function reportConnectionError( $error = 'Unknown error' );
-
/**
* Run an SQL query and return the result. Normally throws a DBQueryError
* on failure. If errors are ignored, returns false instead.
*/
public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
- /**
- * Report a query error. Log the error, and if neither the object ignore
- * flag nor the $tempIgnore flag is set, throw a DBQueryError.
- *
- * @param string $error
- * @param int $errno
- * @param string $sql
- * @param string $fname
- * @param bool $tempIgnore
- * @throws DBQueryError
- */
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false );
-
/**
* Free a result object returned by query() or select(). It's usually not
* necessary to call this, just use unset() or let the variable holding
*/
public function tableExists( $table, $fname = __METHOD__ );
- /**
- * Determines if a given index is unique
- *
- * @param string $table
- * @param string $index
- *
- * @return bool
- */
- public function indexUnique( $table, $index );
-
/**
* INSERT wrapper, inserts an array into a table.
*
*/
public function flushSnapshot( $fname = __METHOD__ );
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- * @throws DBError
- * @return array
- */
- public function listTables( $prefix = null, $fname = __METHOD__ );
-
/**
* Convert a timestamp in one of the formats accepted by wfTimestamp()
* to the format used for inserting into timestamp fields in this DBMS.
* @since 1.29
*/
public function unlockTables( $method );
+
+ /**
+ * List all tables on the database
+ *
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname Calling function name
+ * @throws DBError
+ * @return array
+ */
+ public function listTables( $prefix = null, $fname = __METHOD__ );
+
+ /**
+ * Determines if a given index is unique
+ *
+ * @param string $table
+ * @param string $index
+ *
+ * @return bool
+ */
+ public function indexUnique( $table, $index );
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param string $table Table name
+ * @param string $field Field name
+ *
+ * @return Field
+ */
+ public function fieldInfo( $table, $field );
}
class_alias( IMaintainableDatabase::class, 'IMaintainableDatabase' );
public function unlockTables( $method ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
+
+ public function indexUnique( $table, $index ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function listTables( $prefix = null, $fname = __METHOD__ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function fieldInfo( $table, $field ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
}
class_alias( MaintainableDBConnRef::class, 'MaintainableDBConnRef' );
* - Binlog-based usage assumes single-source replication and non-hierarchical replication.
* - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
* that GTID sets are complete (e.g. include all domains on the server).
+ *
+ * @see https://mariadb.com/kb/en/library/gtid/
+ * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
*/
class MySQLMasterPos implements DBMasterPos {
- /** @var string|null Binlog file base name */
- public $binlog;
- /** @var int[]|null Binglog file position tuple */
- public $pos;
- /** @var string[] GTID list */
- public $gtids = [];
+ /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
+ private $style;
+ /** @var string|null Base name of all Binary Log files */
+ private $binLog;
+ /** @var int[]|null Binary Log position tuple (index number, event number) */
+ private $logPos;
+ /** @var string[] Map of (server_uuid/gtid_domain_id => GTID) */
+ private $gtids = [];
+ /** @var int|null Active GTID domain ID */
+ private $activeDomain;
+ /** @var int|null ID of the server were DB writes originate */
+ private $activeServerId;
+ /** @var string|null UUID of the server were DB writes originate */
+ private $activeServerUUID;
/** @var float UNIX timestamp */
- public $asOfTime = 0.0;
+ private $asOfTime = 0.0;
+
+ const BINARY_LOG = 'binary-log';
+ const GTID_MARIA = 'gtid-maria';
+ const GTID_MYSQL = 'gtid-mysql';
+
+ /** @var int Key name of the binary log index number of a position tuple */
+ const CORD_INDEX = 0;
+ /** @var int Key name of the binary log event number of a position tuple */
+ const CORD_EVENT = 1;
/**
* @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
protected function init( $position, $asOfTime ) {
$m = [];
if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
- $this->binlog = $m[1]; // ideally something like host name
- $this->pos = [ (int)$m[2], (int)$m[3] ];
+ $this->binLog = $m[1]; // ideally something like host name
+ $this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ];
+ $this->style = self::BINARY_LOG;
} else {
$gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
foreach ( $gtids as $gtid ) {
- if ( !self::parseGTID( $gtid ) ) {
+ $components = self::parseGTID( $gtid );
+ if ( !$components ) {
throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
}
- $this->gtids[] = $gtid;
+
+ list( $domain, $pos ) = $components;
+ if ( isset( $this->gtids[$domain] ) ) {
+ // For MySQL, handle the case where some past issue caused a gap in the
+ // executed GTID set, e.g. [last_purged+1,N-1] and [N+1,N+2+K]. Ignore the
+ // gap by using the GTID with the highest ending sequence number.
+ list( , $otherPos ) = self::parseGTID( $this->gtids[$domain] );
+ if ( $pos > $otherPos ) {
+ $this->gtids[$domain] = $gtid;
+ }
+ } else {
+ $this->gtids[$domain] = $gtid;
+ }
+
+ if ( is_int( $domain ) ) {
+ $this->style = self::GTID_MARIA; // gtid_domain_id
+ } else {
+ $this->style = self::GTID_MYSQL; // server_uuid
+ }
}
if ( !$this->gtids ) {
- throw new InvalidArgumentException( "Got empty GTID set." );
+ throw new InvalidArgumentException( "GTID set cannot be empty." );
}
}
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosByDomain = $this->getGtidCoordinates();
- $thatPosByDomain = $pos->getGtidCoordinates();
+ $thisPosByDomain = $this->getActiveGtidCoordinates();
+ $thatPosByDomain = $pos->getActiveGtidCoordinates();
if ( $thisPosByDomain && $thatPosByDomain ) {
$comparisons = [];
// Check that this has positions reaching those in $pos for all domains in common
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosDomains = array_keys( $this->getGtidCoordinates() );
- $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
+ $thisPosDomains = array_keys( $this->getActiveGtidCoordinates() );
+ $thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() );
if ( $thisPosDomains && $thatPosDomains ) {
// Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
// quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
}
/**
- * @return string|null
+ * @return string|null Base name of binary log files
+ * @since 1.31
+ */
+ public function getLogName() {
+ return $this->gtids ? null : $this->binLog;
+ }
+
+ /**
+ * @return int[]|null Tuple of (binary log file number, event number)
+ * @since 1.31
+ */
+ public function getLogPosition() {
+ return $this->gtids ? null : $this->logPos;
+ }
+
+ /**
+ * @return string|null Name of the binary log file for this position
+ * @since 1.31
*/
public function getLogFile() {
- return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}";
+ return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}";
}
/**
- * @return string[]
+ * @return string[] Map of (server_uuid/gtid_domain_id => GTID)
+ * @since 1.31
*/
public function getGTIDs() {
return $this->gtids;
}
/**
- * @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
+ * @param int|null $id @@gtid_domain_id of the active replication stream
+ * @since 1.31
*/
- public function __toString() {
- return $this->gtids
- ? implode( ',', $this->gtids )
- : $this->getLogFile() . "/{$this->pos[1]}";
+ public function setActiveDomain( $id ) {
+ $this->activeDomain = (int)$id;
+ }
+
+ /**
+ * @param int|null $id @@server_id of the server were writes originate
+ * @since 1.31
+ */
+ public function setActiveOriginServerId( $id ) {
+ $this->activeServerId = (int)$id;
+ }
+
+ /**
+ * @param string|null $id @@server_uuid of the server were writes originate
+ * @since 1.31
+ */
+ public function setActiveOriginServerUUID( $id ) {
+ $this->activeServerUUID = $id;
}
/**
* @param MySQLMasterPos $pos
* @param MySQLMasterPos $refPos
* @return string[] List of GTIDs from $pos that have domains in $refPos
+ * @since 1.31
*/
public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
- $gtidsCommon = [];
-
- $relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused)
- foreach ( $pos->gtids as $gtid ) {
- list( $domain ) = self::parseGTID( $gtid );
- if ( isset( $relevantDomains[$domain] ) ) {
- $gtidsCommon[] = $gtid;
- }
- }
-
- return $gtidsCommon;
+ return array_values(
+ array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() )
+ );
}
/**
* @see https://mariadb.com/kb/en/mariadb/gtid
* @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
- * @return array Map of (domain => integer position); possibly empty
+ * @return array Map of (server_uuid/gtid_domain_id => integer position); possibly empty
*/
- protected function getGtidCoordinates() {
+ protected function getActiveGtidCoordinates() {
$gtidInfos = [];
- foreach ( $this->gtids as $gtid ) {
- list( $domain, $pos ) = self::parseGTID( $gtid );
- $gtidInfos[$domain] = $pos;
+
+ foreach ( $this->gtids as $domain => $gtid ) {
+ list( $domain, $pos, $server ) = self::parseGTID( $gtid );
+
+ $ignore = false;
+ // Filter out GTIDs from non-active replication domains
+ if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
+ $ignore |= ( $domain !== $this->activeDomain );
+ }
+ // Likewise for GTIDs from non-active replication origin servers
+ if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
+ $ignore |= ( $server !== $this->activeServerId );
+ } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
+ $ignore |= ( $server !== $this->activeServerUUID );
+ }
+
+ if ( !$ignore ) {
+ $gtidInfos[$domain] = $pos;
+ }
}
return $gtidInfos;
}
/**
- * @param string $gtid
- * @return array|null [domain, integer position] or null
+ * @param string $id GTID
+ * @return array|null [domain ID or server UUID, sequence number, server ID/UUID] or null
*/
- protected static function parseGTID( $gtid ) {
+ protected static function parseGTID( $id ) {
$m = [];
- if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
+ if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
// MariaDB style: <domain>-<server id>-<sequence number>
- return [ (int)$m[1], (int)$m[2] ];
- } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
- // MySQL style: <UUID domain>:<sequence number>
- return [ $m[1], (int)$m[2] ];
+ return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
+ } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
+ // MySQL style: <server UUID>:<sequence number>-<sequence number>
+ // Normally, the first number should reflect the point (gtid_purged) where older
+ // binary logs where purged to save space. When doing comparisons, it may as well
+ // be 1 in that case. Assume that this is generally the situation.
+ return [ $m[1], (int)$m[2], $m[1] ];
}
return null;
/**
* @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
* @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
- * @return array|bool (binlog, (integer file number, integer position)) or false
+ * @return array|bool Map of (binlog:<string>, pos:(<integer>, <integer>)) or false
*/
protected function getBinlogCoordinates() {
- return ( $this->binlog !== null && $this->pos !== null )
- ? [ 'binlog' => $this->binlog, 'pos' => $this->pos ]
+ return ( $this->binLog !== null && $this->logPos !== null )
+ ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
: false;
}
$this->init( $data['position'], $data['asOfTime'] );
}
+
+ /**
+ * @return string GTID set or <binary log file>/<position> (e.g db1034-bin.000976/843431247)
+ */
+ public function __toString() {
+ return $this->gtids
+ ? implode( ',', $this->gtids )
+ : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
+ }
}
return [
'digitTransformTable' => $language->digitTransformTable(),
'separatorTransformTable' => $language->separatorTransformTable(),
+ 'minimumGroupingDigits' => $language->minimumGroupingDigits(),
'grammarForms' => $language->getGrammarForms(),
'grammarTransformations' => $language->getGrammarTransformations(),
'pluralRules' => $language->getPluralRules(),
<?php
/**
- * Module for ResourceLoader initialization.
- *
* 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
* @author Roan Kattouw
*/
+/**
+ * Module for ResourceLoader initialization.
+ *
+ * See also <https://www.mediawiki.org/wiki/ResourceLoader/Features#Startup_Module>
+ *
+ * The startup module, as being called only from ResourceLoaderClientHtml, has
+ * the ability to vary based extra query parameters, in addition to those
+ * from ResourceLoaderContext:
+ *
+ * - target: Only register modules in the client allowed within this target.
+ * Default: "desktop".
+ * See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
+ */
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Cache for getConfigSettings() as it's called by multiple methods
$this->mHideName = $block->mHideName;
$this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
} else {
+ $this->mBlock = null;
$this->mBlockedby = '';
+ $this->mBlockreason = '';
$this->mHideName = 0;
$this->mAllowUsertalk = false;
}
* @param string $oname The option to check
* @param string $defaultOverride A default value returned if the option does not exist
* @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
- * @return string|null User's current value for the option
+ * @return string|array|int|null User's current value for the option
* @see getBoolOption()
* @see getIntOption()
*/
return;
}
- $dbw = wfGetDB( DB_MASTER );
- $asOfTimes = array_unique( $dbw->selectFieldValues(
- 'watchlist',
- 'wl_notificationtimestamp',
- [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
- __METHOD__,
- [ 'ORDER BY' => 'wl_notificationtimestamp DESC', 'LIMIT' => 500 ]
- ) );
- if ( !$asOfTimes ) {
- return;
- }
- // Immediately update the most recent touched rows, which hopefully covers what
- // the user sees on the watchlist page before pressing "mark all pages visited"....
- $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => null ],
- [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimes ],
- __METHOD__
- );
- // ...and finish the older ones in a post-send update with lag checks...
- DeferredUpdates::addUpdate( new AutoCommitUpdate(
- $dbw,
- __METHOD__,
- function () use ( $dbw, $id ) {
- global $wgUpdateRowsPerQuery;
-
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $asOfTimes = array_unique( $dbw->selectFieldValues(
- 'watchlist',
- 'wl_notificationtimestamp',
- [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
- __METHOD__
- ) );
- foreach ( array_chunk( $asOfTimes, $wgUpdateRowsPerQuery ) as $asOfTimeBatch ) {
- $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => null ],
- [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimeBatch ],
- __METHOD__
- );
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
- }
- }
- ) );
+ $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
+ $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
+
// We also need to clear here the "you have new message" notification for the own
// user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
}
throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
}
+ public function resetAllNotificationTimestampsForUser( User $user ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
public function resetNotificationTimestamp(
User $user,
Title $title,
* @since 1.27
* @param User $user
* @param LinkTarget $target
- * @return bool
+ * @return WatchedItem|bool
*/
public function loadWatchedItem( User $user, LinkTarget $target ) {
// Only loggedin user can have a watchlist
return $success;
}
+ public function resetAllNotificationTimestampsForUser( User $user ) {
+ // Only loggedin user can have a watchlist
+ if ( $user->isAnon() ) {
+ return;
+ }
+
+ // If the page is watched by the user (or may be watched), update the timestamp
+ $job = new ClearWatchlistNotificationsJob(
+ $user->getUserPage(),
+ [ 'userId' => $user->getId(), 'casTime' => time() ]
+ );
+
+ // Try to run this post-send
+ // Calls DeferredUpdates::addCallableUpdate in normal operation
+ call_user_func(
+ $this->deferredUpdatesAddCallableUpdateCallback,
+ function () use ( $job ) {
+ $job->run();
+ }
+ );
+ }
+
/**
* @since 1.27
* @param User $editor
* @param LinkTarget $target
* @param string|int $timestamp
- * @return int
+ * @return int[]
*/
public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
$dbw = $this->getConnectionRef( DB_MASTER );
/**
* @since 1.31
*
- * @param User $user The user to set the timestamp for
+ * @param User $user The user to set the timestamps for
* @param string|null $timestamp Set the update timestamp to this value
* @param LinkTarget[] $targets List of targets to update. Default to all targets
*
array $targets = []
);
+ /**
+ * Reset all watchlist notificaton timestamps for a user using the job queue
+ *
+ * @since 1.31
+ *
+ * @param User $user The user to reset the timestamps for
+ */
+ public function resetAllNotificationTimestampsForUser( User $user );
+
/**
* @since 1.31
*
* @param int $oldid The revision id being viewed. If not given or 0, latest revision is
* assumed.
*
- * @return bool success
+ * @return bool success Whether a job was enqueued
*/
public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 );
'class' => ResourceLoaderOOUIFileModule::class,
'styles' => [
'resources/lib/oojs-ui/wikimedia-ui-base.less', // Providing Wikimedia UI LESS variables to all
- 'resources/src/oojs-ui-local.css', // HACK, see inside the file
],
'themeStyles' => 'core',
'targets' => [ 'desktop', 'mobile' ],
*
* - `digitTransformTable`
* - `separatorTransformTable`
+ * - `minimumGroupingDigits`
* - `grammarForms`
* - `pluralRules`
* - `digitGroupingPattern`
* @private
* @param {number} value the number to be formatted, ignores sign
* @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`)
- * @param {Object} [options] If provided, both option keys must be present:
+ * @param {Object} [options] If provided, all option keys must be present:
* @param {string} options.decimal The decimal separator. Defaults to: `'.'`.
* @param {string} options.group The group separator. Defaults to: `','`.
+ * @param {number|null} options.minimumGroupingDigits
* @return {string}
*/
function commafyNumber( value, pattern, options ) {
}
}
- for ( whole = valueParts[ 0 ]; whole; ) {
- off = groupSize ? whole.length - groupSize : 0;
- pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
- whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
-
- if ( groupSize2 ) {
- groupSize = groupSize2;
- groupSize2 = null;
+ if (
+ options.minimumGroupingDigits === null ||
+ valueParts[ 0 ].length >= groupSize + options.minimumGroupingDigits
+ ) {
+ for ( whole = valueParts[ 0 ]; whole; ) {
+ off = groupSize ? whole.length - groupSize : 0;
+ pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
+ whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
+
+ if ( groupSize2 ) {
+ groupSize = groupSize2;
+ groupSize2 = null;
+ }
}
+ valueParts[ 0 ] = pieces.reverse().join( options.group );
}
- valueParts[ 0 ] = pieces.reverse().join( options.group );
return valueParts.join( options.decimal );
}
*/
convertNumber: function ( num, integer ) {
var transformTable, digitTransformTable, separatorTransformTable,
- i, numberString, convertedNumber, pattern;
+ i, numberString, convertedNumber, pattern, minimumGroupingDigits;
// Quick shortcut for plain numbers
if ( integer && parseInt( num, 10 ) === num ) {
// When unformatting, we just use separatorTransformTable.
pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
'digitGroupingPattern' ) || '#,##0.###';
- numberString = mw.language.commafy( num, pattern );
+ minimumGroupingDigits = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'minimumGroupingDigits' ) || null;
+ numberString = mw.language.commafy( num, pattern, minimumGroupingDigits );
}
if ( transformTable ) {
*
* @param {number} value
* @param {string} pattern Pattern string as described by Unicode TR35
+ * @param {number|null} [minimumGroupingDigits=null]
* @throws {Error} If unable to find a number expression in `pattern`.
* @return {string}
*/
- commafy: function ( value, pattern ) {
+ commafy: function ( value, pattern, minimumGroupingDigits ) {
var numberPattern,
transformTable = mw.language.getSeparatorTransformTable(),
group = transformTable[ ',' ] || ',',
pattern = patternList[ ( value < 0 ) ? 1 : 0 ] || ( '-' + positivePattern );
numberPattern = positivePattern.match( numberPatternRE );
+ minimumGroupingDigits = minimumGroupingDigits !== undefined ? minimumGroupingDigits : null;
if ( !numberPattern ) {
throw new Error( 'unable to find a number expression in pattern: ' + pattern );
}
return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
+ minimumGroupingDigits: minimumGroupingDigits,
decimal: decimal,
group: group
} ) );
// Parent
mw.rcfilters.ui.MarkSeenButtonWidget.parent.call( this, $.extend( {
label: mw.message( 'rcfilters-watchlist-markseen-button' ).text(),
- icon: 'doubleCheck'
+ icon: 'checkAll'
}, config ) );
this.controller = controller;
width: 100%;
border: 1px solid @colorFieldBorder;
border-radius: @borderRadius;
- padding: 0.625em 0.625em 0.546875em;
+ padding: 0.57142857em 0.57142857em 0.5em;
// necessary for smooth transition
box-shadow: inset 0 0 0 0.1em #fff;
font-family: inherit;
font-size: inherit;
- line-height: 1.172em;
+ line-height: 1.07142857em;
vertical-align: middle;
// Normalize & style placeholder text, see T139034
+++ /dev/null
-/* HACK: Set sane font-size for OOUI dialogs (and menus/popups inside the default overlay), in
- the most common case. This should be skin's responsibility, but alas our skins tend to have the
- weirdest font-sizes on body. This shall be removed when we make the MediaWiki skins bundled with
- tarball sane. (T91152) */
-body > .oo-ui-windowManager,
-.oo-ui-defaultOverlay {
- font-size: 0.8rem;
-}
$this->assertEquals( 'Success', $a );
}
+ public function testLoginWithNoSameOriginSecurity() {
+ $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
+ function () {
+ return false;
+ }
+ );
+
+ $result = $this->doApiRequest( [
+ 'action' => 'login',
+ ] )[0]['login'];
+
+ $this->assertSame( [
+ 'result' => 'Aborted',
+ 'reason' => 'Cannot log in when the same-origin policy is not applied.',
+ ], $result );
+ }
}
private static $allcategories = [
[ 'list' => 'allcategories', 'acprefix' => 'AQBT-' ],
[ 'allcategories' => [
- [ '*' => 'AQBT-Cat' ],
+ [ 'category' => 'AQBT-Cat' ],
] ]
];
$this->check( self::$allpages );
$this->check( self::$alllinks );
$this->check( self::$alltransclusions );
- // This test is temporarily disabled until a sqlite bug is fixed
- // Confirmed still broken 15-nov-2013
- // $this->check( self::$allcategories );
+ $this->check( self::$allcategories );
$this->check( self::$backlinks );
$this->check( self::$embeddedin );
$this->check( self::$categorymembers );
$db->listViews( '' ) );
}
+ /**
+ * @covers Wikimedia\Rdbms\MySQLMasterPos
+ */
public function testBinLogName() {
$pos = new MySQLMasterPos( "db1052.2424/4643", 1 );
- $this->assertEquals( "db1052", $pos->binlog );
+ $this->assertEquals( "db1052", $pos->getLogName() );
$this->assertEquals( "db1052.2424", $pos->getLogFile() );
- $this->assertEquals( [ 2424, 4643 ], $pos->pos );
+ $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() );
}
/**
],
// MySQL GTID style
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:23', $now ),
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:24', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
true,
false
],
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
true,
false
],
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
- new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
+ new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
false,
false
],
],
[
new MySQLMasterPos(
- '2E11FA47-71CA-11E1-9E33-C80AA9429562:5,' .
- '3E11FA47-71CA-11E1-9E33-C80AA9429562:99,' .
- '7E11FA47-71CA-11E1-9E33-C80AA9429562:30',
+ '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
+ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
+ '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
1
),
new MySQLMasterPos(
- '1E11FA47-71CA-11E1-9E33-C80AA9429562:100,' .
- '3E11FA47-71CA-11E1-9E33-C80AA9429562:66',
+ '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
+ '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
1
),
- [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ]
+ [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ]
]
];
}
];
}
+ /**
+ * @dataProvider provideGtidData
+ * @covers Wikimedia\Rdbms\MySQLMasterPos
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos
+ */
+ public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) {
+ $db = $this->getMockBuilder( DatabaseMysqli::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [
+ 'useGTIDs',
+ 'getServerGTIDs',
+ 'getServerRoleStatus',
+ 'getServerId',
+ 'getServerUUID'
+ ] )
+ ->getMock();
+
+ $db->method( 'useGTIDs' )->willReturn( true );
+ $db->method( 'getServerGTIDs' )->willReturn( $gtable );
+ $db->method( 'getServerRoleStatus' )->willReturnCallback(
+ function ( $role ) use ( $rBLtable, $mBLtable ) {
+ if ( $role === 'SLAVE' ) {
+ return $rBLtable;
+ } elseif ( $role === 'MASTER' ) {
+ return $mBLtable;
+ }
+
+ return null;
+ }
+ );
+ $db->method( 'getServerId' )->willReturn( 1 );
+ $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' );
+
+ if ( is_array( $rGTIDs ) ) {
+ $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() );
+ } else {
+ $this->assertEquals( false, $db->getReplicaPos() );
+ }
+ if ( is_array( $mGTIDs ) ) {
+ $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() );
+ } else {
+ $this->assertEquals( false, $db->getMasterPos() );
+ }
+ }
+
+ public static function provideGtidData() {
+ return [
+ // MariaDB
+ [
+ [
+ 'gtid_domain_id' => 100,
+ 'gtid_current_pos' => '100-13-77',
+ 'gtid_binlog_pos' => '100-13-77',
+ 'gtid_slave_pos' => null // master
+ ],
+ [],
+ [
+ 'File' => 'host.1600',
+ 'Pos' => '77'
+ ],
+ [ '100' => '100-13-77' ],
+ [ '100' => '100-13-77' ]
+ ],
+ [
+ [
+ 'gtid_domain_id' => 100,
+ 'gtid_current_pos' => '100-13-77',
+ 'gtid_binlog_pos' => '100-13-77',
+ 'gtid_slave_pos' => '100-13-77' // replica
+ ],
+ [
+ 'Relay_Master_Log_File' => 'host.1600',
+ 'Exec_Master_Log_Pos' => '77'
+ ],
+ [],
+ [ '100' => '100-13-77' ],
+ [ '100' => '100-13-77' ]
+ ],
+ [
+ [
+ 'gtid_current_pos' => '100-13-77',
+ 'gtid_binlog_pos' => '100-13-77',
+ 'gtid_slave_pos' => '100-13-77' // replica
+ ],
+ [
+ 'Relay_Master_Log_File' => 'host.1600',
+ 'Exec_Master_Log_Pos' => '77'
+ ],
+ [],
+ [ '100' => '100-13-77' ],
+ [ '100' => '100-13-77' ]
+ ],
+ // MySQL
+ [
+ [
+ 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77'
+ ],
+ [
+ 'Relay_Master_Log_File' => 'host.1600',
+ 'Exec_Master_Log_Pos' => '77'
+ ],
+ [], // only a replica
+ [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+ => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
+ // replica/master use same var
+ [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+ => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
+ ],
+ [
+ [
+ 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' .
+ '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77'
+ ],
+ [
+ 'Relay_Master_Log_File' => 'host.1600',
+ 'Exec_Master_Log_Pos' => '77'
+ ],
+ [], // only a replica
+ [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+ => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
+ // replica/master use same var
+ [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+ => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
+ ],
+ [
+ [
+ 'gtid_executed' => null // not enabled?
+ ],
+ [
+ 'Relay_Master_Log_File' => 'host.1600',
+ 'Exec_Master_Log_Pos' => '77'
+ ],
+ [], // only a replica
+ [], // binlog fallback
+ false
+ ],
+ [
+ [
+ 'gtid_executed' => null // not enabled?
+ ],
+ [], // no replication
+ [], // no replication
+ false,
+ false
+ ]
+ ];
+ }
+
/**
* @covers Wikimedia\Rdbms\MySQLMasterPos
*/
$user->setOption( 'userjs-someoption', 'test' );
$user->setOption( 'rclimit', 200 );
+ $user->setOption( 'wpwatchlistdays', '0' );
$user->saveSettings();
$user = User::newFromName( $user->getName() );
MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
$this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) );
$this->assertEquals( 200, $user->getOption( 'rclimit' ) );
+
+ // Check that an option saved as a string '0' is returned as an integer.
+ $user = User::newFromName( $user->getName() );
+ $user->load( User::READ_LATEST );
+ $this->assertSame( 0, $user->getOption( 'wpwatchlistdays' ) );
}
/**
} catch ( InvalidArgumentException $ex ) {
}
}
+
+ /**
+ * @covers User::getBlockedStatus
+ * @covers User::getBlock
+ * @covers User::blockedBy
+ * @covers User::blockedFor
+ * @covers User::isHidden
+ * @covers User::isBlockedFrom
+ */
+ public function testBlockInstanceCache() {
+ // First, check the user isn't blocked
+ $user = $this->getMutableTestUser()->getUser();
+ $ut = Title::makeTitle( NS_USER_TALK, $user->getName() );
+ $this->assertNull( $user->getBlock( false ), 'sanity check' );
+ $this->assertSame( '', $user->blockedBy(), 'sanity check' );
+ $this->assertSame( '', $user->blockedFor(), 'sanity check' );
+ $this->assertFalse( (bool)$user->isHidden(), 'sanity check' );
+ $this->assertFalse( $user->isBlockedFrom( $ut ), 'sanity check' );
+
+ // Block the user
+ $blocker = $this->getTestSysop()->getUser();
+ $block = new Block( [
+ 'hideName' => true,
+ 'allowUsertalk' => false,
+ 'reason' => 'Because',
+ ] );
+ $block->setTarget( $user );
+ $block->setBlocker( $blocker );
+ $res = $block->insert();
+ $this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' );
+
+ // Clear cache and confirm it loaded the block properly
+ $user->clearInstanceCache();
+ $this->assertInstanceOf( Block::class, $user->getBlock( false ) );
+ $this->assertSame( $blocker->getName(), $user->blockedBy() );
+ $this->assertSame( 'Because', $user->blockedFor() );
+ $this->assertTrue( (bool)$user->isHidden() );
+ $this->assertTrue( $user->isBlockedFrom( $ut ) );
+
+ // Unblock
+ $block->delete();
+
+ // Clear cache and confirm it loaded the not-blocked properly
+ $user->clearInstanceCache();
+ $this->assertNull( $user->getBlock( false ) );
+ $this->assertSame( '', $user->blockedBy() );
+ $this->assertSame( '', $user->blockedFor() );
+ $this->assertFalse( (bool)$user->isHidden() );
+ $this->assertFalse( $user->isBlockedFrom( $ut ) );
+ }
+
}
mw.language.setData( 'en', 'digitGroupingPattern', null );
mw.language.setData( 'en', 'digitTransformTable', null );
mw.language.setData( 'en', 'separatorTransformTable', { ',': '.', '.': ',' } );
+ mw.language.setData( 'en', 'minimumGroupingDigits', null );
mw.config.set( 'wgUserLanguage', 'en' );
mw.config.set( 'wgTranslateNumerals', true );
- assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting' );
+ assert.equal( mw.language.convertNumber( 180 ), '180', 'formatting 3-digit' );
+ assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting 4-digit' );
+ assert.equal( mw.language.convertNumber( 18000 ), '18.000', 'formatting 5-digit' );
+
assert.equal( mw.language.convertNumber( '1.800', true ), '1800', 'unformatting' );
+
+ mw.language.setData( 'en', 'minimumGroupingDigits', 2 );
+ assert.equal( mw.language.convertNumber( 180 ), '180', 'formatting 3-digit with minimumGroupingDigits=2' );
+ assert.equal( mw.language.convertNumber( 1800 ), '1800', 'formatting 4-digit with minimumGroupingDigits=2' );
+ assert.equal( mw.language.convertNumber( 18000 ), '18.000', 'formatting 5-digit with minimumGroupingDigits=2' );
} );
QUnit.test( 'mw.language.convertNumber - digitTransformTable', function ( assert ) {
mw.config.set( 'wgTranslateNumerals', true );
mw.language.setData( 'hi', 'digitGroupingPattern', null );
mw.language.setData( 'hi', 'separatorTransformTable', { ',': '.', '.': ',' } );
+ mw.language.setData( 'hi', 'minimumGroupingDigits', null );
// Example from Hindi (MessagesHi.php)
mw.language.setData( 'hi', 'digitTransformTable', {