'WikiRevision' => __DIR__ . '/includes/import/WikiRevision.php',
'WikiStatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php',
'WikiTextStructure' => __DIR__ . '/includes/content/WikiTextStructure.php',
+ 'Wikimedia\\Rdbms\\ConnectionManager' => __DIR__ . '/includes/libs/rdbms/connectionmanager/ConnectionManager.php',
+ 'Wikimedia\\Rdbms\\SessionConsistentConnectionManager' => __DIR__ . '/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php',
'WikitextContent' => __DIR__ . '/includes/content/WikitextContent.php',
'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php',
'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php',
"ext-xml": "*",
"liuggio/statsd-php-client": "1.0.18",
"mediawiki/at-ease": "1.1.0",
- "oojs/oojs-ui": "0.18.0",
+ "oojs/oojs-ui": "0.18.1",
"oyejorge/less.php": "1.7.0.10",
"php": ">=5.5.9",
"psr/log": "1.0.0",
--- /dev/null
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use Database;
+use DBConnRef;
+use IDatabase;
+use InvalidArgumentException;
+use LoadBalancer;
+
+/**
+ * Database connection manager.
+ *
+ * This manages access to master and replica databases.
+ *
+ * @since 1.29
+ *
+ * @license GPL-2.0+
+ * @author Addshore
+ */
+class ConnectionManager {
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * The symbolic name of the target database, or false for the local wiki's database.
+ *
+ * @var string|false
+ */
+ private $domain;
+
+ /**
+ * @var string[]
+ */
+ private $groups = [];
+
+ /**
+ * @param LoadBalancer $loadBalancer
+ * @param string|bool $domain Optional logical DB name, defaults to current wiki.
+ * This follows the convention for database names used by $loadBalancer.
+ * @param string[] $groups see LoadBalancer::getConnection
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct( LoadBalancer $loadBalancer, $domain = false, array $groups = [] ) {
+ if ( !is_string( $domain ) && $domain !== false ) {
+ throw new InvalidArgumentException( '$dbName must be a string, or false.' );
+ }
+
+ $this->loadBalancer = $loadBalancer;
+ $this->domain = $domain;
+ $this->groups = $groups;
+ }
+
+ /**
+ * @param int $i
+ * @param string[]|null $groups
+ *
+ * @return Database
+ */
+ private function getConnection( $i, array $groups = null ) {
+ $groups = $groups === null ? $this->groups : $groups;
+ return $this->loadBalancer->getConnection( $i, $groups, $this->domain );
+ }
+
+ /**
+ * @param int $i
+ * @param string[]|null $groups
+ *
+ * @return DBConnRef
+ */
+ private function getConnectionRef( $i, array $groups = null ) {
+ $groups = $groups === null ? $this->groups : $groups;
+ return $this->loadBalancer->getConnectionRef( $i, $groups, $this->domain );
+ }
+
+ /**
+ * Returns a connection to the master DB, for updating. The connection should later be released
+ * by calling releaseConnection().
+ *
+ * @since 1.29
+ *
+ * @return Database
+ */
+ public function getWriteConnection() {
+ return $this->getConnection( DB_MASTER );
+ }
+
+ /**
+ * Returns a database connection for reading. The connection should later be released by
+ * calling releaseConnection().
+ *
+ * @since 1.29
+ *
+ * @param string[]|null $groups
+ *
+ * @return Database
+ */
+ public function getReadConnection( array $groups = null ) {
+ $groups = $groups === null ? $this->groups : $groups;
+ return $this->getConnection( DB_REPLICA, $groups );
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param IDatabase $db
+ */
+ public function releaseConnection( IDatabase $db ) {
+ $this->loadBalancer->reuseConnection( $db );
+ }
+
+ /**
+ * Returns a connection ref to the master DB, for updating.
+ *
+ * @since 1.29
+ *
+ * @return DBConnRef
+ */
+ public function getWriteConnectionRef() {
+ return $this->getConnectionRef( DB_MASTER );
+ }
+
+ /**
+ * Returns a database connection ref for reading.
+ *
+ * @since 1.29
+ *
+ * @param string[]|null $groups
+ *
+ * @return DBConnRef
+ */
+ public function getReadConnectionRef( array $groups = null ) {
+ $groups = $groups === null ? $this->groups : $groups;
+ return $this->getConnectionRef( DB_REPLICA, $groups );
+ }
+
+ /**
+ * Begins an atomic section and returns a database connection to the master DB, for updating.
+ *
+ * @since 1.29
+ *
+ * @param string $fname
+ *
+ * @return Database
+ */
+ public function beginAtomicSection( $fname ) {
+ $db = $this->getWriteConnection();
+ $db->startAtomic( $fname );
+
+ return $db;
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param IDatabase $db
+ * @param string $fname
+ */
+ public function commitAtomicSection( IDatabase $db, $fname ) {
+ $db->endAtomic( $fname );
+ $this->releaseConnection( $db );
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param IDatabase $db
+ * @param string $fname
+ */
+ public function rollbackAtomicSection( IDatabase $db, $fname ) {
+ // FIXME: there does not seem to be a clean way to roll back an atomic section?!
+ $db->rollback( $fname, 'flush' );
+ $this->releaseConnection( $db );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use Database;
+use DBConnRef;
+
+/**
+ * Database connection manager.
+ *
+ * This manages access to master and slave databases. It also manages state that indicates whether
+ * the slave databases are possibly outdated after a write operation, and thus the master database
+ * should be used for subsequent read operations.
+ *
+ * @note: Services that access overlapping sets of database tables, or interact with logically
+ * related sets of data in the database, should share a SessionConsistentConnectionManager.
+ * Services accessing unrelated sets of information may prefer to not share a
+ * SessionConsistentConnectionManager, so they can still perform read operations against slave
+ * databases after a (unrelated, per the assumption) write operation to the master database.
+ * Generally, sharing a SessionConsistentConnectionManager improves consistency (by avoiding race
+ * conditions due to replication lag), but can reduce performance (by directing more read
+ * operations to the master database server).
+ *
+ * @since 1.29
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ * @author Addshore
+ */
+class SessionConsistentConnectionManager extends ConnectionManager {
+
+ /**
+ * @var bool
+ */
+ private $forceWriteConnection = false;
+
+ /**
+ * Forces all future calls to getReadConnection() to return a write connection.
+ * Use this before performing read operations that are critical for a future update.
+ * Calling beginAtomicSection() implies a call to prepareForUpdates().
+ *
+ * @since 1.29
+ */
+ public function prepareForUpdates() {
+ $this->forceWriteConnection = true;
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param string[]|null $groups
+ *
+ * @return Database
+ */
+ public function getReadConnection( array $groups = null ) {
+ if ( $this->forceWriteConnection ) {
+ return parent::getWriteConnection();
+ }
+
+ return parent::getReadConnection( $groups );
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @return Database
+ */
+ public function getWriteConnection() {
+ $this->prepareForUpdates();
+ return parent::getWriteConnection();
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param string[]|null $groups
+ *
+ * @return DBConnRef
+ */
+ public function getReadConnectionRef( array $groups = null ) {
+ if ( $this->forceWriteConnection ) {
+ return parent::getWriteConnectionRef();
+ }
+
+ return parent::getReadConnectionRef( $groups );
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @return DBConnRef
+ */
+ public function getWriteConnectionRef() {
+ $this->prepareForUpdates();
+ return parent::getWriteConnectionRef();
+ }
+
+ /**
+ * Begins an atomic section and returns a database connection to the master DB, for updating.
+ *
+ * @since 1.29
+ *
+ * @note: This causes all future calls to getReadConnection() to return a connection
+ * to the master DB, even after commitAtomicSection() or rollbackAtomicSection() have
+ * been called.
+ *
+ * @param string $fname
+ *
+ * @return Database
+ */
+ public function beginAtomicSection( $fname ) {
+ // Once we have written to master, do not read from replica.
+ $this->prepareForUpdates();
+
+ return parent::beginAtomicSection( $fname );
+ }
+
+}
$this->mImg = null;
$this->mHist = [];
$this->mRange = [ 0, 0 ]; // display range
+
+ // Only display 10 revisions at once by default, otherwise the list is overwhelming
+ $this->mLimitsShown = array_merge( [ 10 ], $this->mLimitsShown );
+ $this->setLimit( 10 );
}
/**
$str .= $this->getDelimiter();
}
- return $str . $this->hash;
+ $res = $str . $this->hash;
+ $this->assertIsSafeSize( $res );
+ return $res;
}
/**
*/
protected $config;
+ /**
+ * Hash must fit in user_password, which is a tinyblob
+ */
+ const MAX_HASH_SIZE = 255;
+
/**
* Construct the Password object using a string hash
*
* are considered equivalent.
*
* @return string
+ * @throws PasswordError if password cannot be serialized to fit a tinyblob.
*/
public function toString() {
- return ':' . $this->config['type'] . ':' . $this->hash;
+ $result = ':' . $this->config['type'] . ':' . $this->hash;
+ $this->assertIsSafeSize( $result );
+ return $result;
+ }
+
+ /**
+ * Assert that hash will fit in a tinyblob field.
+ *
+ * This prevents MW from inserting it into the DB
+ * and having MySQL silently truncating it, locking
+ * the user out of their account.
+ *
+ * @param string $hash The hash in question.
+ * @throws PasswordError If hash does not fit in DB.
+ */
+ final protected function assertIsSafeSize( $hash ) {
+ if ( strlen( $hash ) > self::MAX_HASH_SIZE ) {
+ throw new PasswordError( "Password hash is too big" );
+ }
}
/**
* @param bool $writing
* @return Status
*/
- public function fetchUser( $username, $writing ) {
+ public function fetchUser( $username, $writing = true ) {
$parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
if ( count( $parts ) < 2 ) {
$name = trim( $username );
return [
'tables' => [ 'category' ],
'fields' => [ 'cat_title', 'cat_pages' ],
- 'conds' => [ 'cat_pages > 0' ],
'options' => [ 'USE INDEX' => 'cat_title' ],
];
}
"ooui-dialog-process-dismiss": "Απόρριψη",
"ooui-dialog-process-retry": "Δοκιμάστε ξανά",
"ooui-dialog-process-continue": "Συνέχεια",
+ "ooui-selectfile-button-select": "Επιλέξτε ένα αρχείο",
"ooui-selectfile-not-supported": "Επιλογή αρχείου δεν υποστηρίζεται",
"ooui-selectfile-placeholder": "Κανένα αρχείο δεν είναι επιλεγμένο",
"ooui-selectfile-dragdrop-placeholder": "Σύρετε το αρχείο εδώ"
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-element-hidden {
display: none !important;
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-element-hidden {
display: none !important;
box-shadow: none;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #c33;
+ color: #d33;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label {
- color: #e53939;
+ color: #ff4242;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #873636;
+ color: #b32424;
box-shadow: none;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled[class*='oo-ui-flaggedElement'] > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- background-color: #d9d9d9;
+ background-color: #c8ccd1;
color: #000;
border-color: #72777d;
}
box-shadow: inset 0 0 0 1px #36c;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
- color: #c33;
+ color: #d33;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
background-color: #fff;
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #fbf4f4;
- color: #873636;
- border-color: #873636;
+ background-color: #ffffff;
+ color: #b32424;
+ border-color: #b32424;
box-shadow: none;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
- border-color: #c33;
- box-shadow: inset 0 0 0 1px #c33;
+ border-color: #d33;
+ box-shadow: inset 0 0 0 1px #d33;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
color: #fff;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
color: #fff;
- background-color: #c33;
- border-color: #c33;
+ background-color: #d33;
+ border-color: #d33;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
- background-color: #e53939;
- border-color: #e53939;
+ background-color: #ff4242;
+ border-color: #ff4242;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
color: #fff;
- background-color: #873636;
- border-color: #873636;
+ background-color: #b32424;
+ border-color: #b32424;
box-shadow: none;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
- border-color: #c33;
- box-shadow: inset 0 0 0 1px #c33, inset 0 0 0 2px #fff;
+ border-color: #d33;
+ box-shadow: inset 0 0 0 1px #d33, inset 0 0 0 2px #fff;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
}
.oo-ui-fieldLayout {
display: block;
- margin-bottom: 1em;
+ margin-top: 1.640625em;
}
.oo-ui-fieldLayout:before,
.oo-ui-fieldLayout:after {
padding: 0.5em 0.75em;
line-height: 1.5;
}
-.oo-ui-fieldLayout:last-child {
- margin-bottom: 0;
+.oo-ui-fieldLayout.oo-ui-labelElement,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+ margin-top: 1.171875em;
}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding-top: 0.5em;
- margin-right: 5%;
- width: 35%;
+.oo-ui-fieldLayout:first-child,
+.oo-ui-fieldLayout.oo-ui-labelElement:first-child,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline:first-child {
+ margin-top: 0;
}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- width: 60%;
+.oo-ui-fieldLayout.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding-bottom: 0.3125em;
}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
- margin-bottom: 1.25em;
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding: 0.3125em 0.46875em;
+}
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ width: 35%;
+ margin-right: 5%;
+ padding-top: 0.3125em;
}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 0.5em;
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ width: 60%;
}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding-top: 0.25em;
- padding-bottom: 0.5em;
+.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ color: #72777d;
}
.oo-ui-fieldLayout > .oo-ui-popupButtonWidget {
margin-right: 0;
.oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child {
margin-right: 0;
}
-.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- color: #72777d;
-}
.oo-ui-fieldLayout-messages {
list-style: none none;
margin: 0.25em 0 0 0.25em;
}
.oo-ui-fieldLayout-messages .oo-ui-iconWidget {
display: table-cell;
- border-right: 0.5em solid transparent;
}
.oo-ui-fieldLayout-messages .oo-ui-labelWidget {
display: table-cell;
- padding: 0.1em 0;
+ padding: 0.1em 0 0.1em 0.3125em;
line-height: 1.5;
vertical-align: middle;
}
margin-top: 2em;
}
.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
- margin-bottom: 0.5em;
+ margin-bottom: 0.56818em;
font-size: 1.1em;
font-weight: bold;
}
background-color: transparent;
}
.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 0.5em;
+ padding: 0.25em 0.25em 0.25em 0.46875em;
}
.oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
margin-right: 0;
}
.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover {
background-color: #fff;
+ color: #444;
border-color: #a2a9b1;
}
.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover .oo-ui-iconElement-icon,
.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover .oo-ui-indicatorElement-indicator {
opacity: 0.73;
}
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:active {
+ color: #000;
+ border-color: #72777d;
+}
.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:focus {
border-color: #36c;
outline: 0;
vertical-align: middle;
}
.oo-ui-checkboxMultioptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 0.5em;
+ padding: 0.25em 0.25em 0.25em 0.46875em;
}
.oo-ui-checkboxMultioptionWidget .oo-ui-checkboxInputWidget {
margin-right: 0;
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
}
return message;
};
-} )();
+}() );
/**
* Package a message and arguments for deferred resolution.
OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
if (
this.isVisible() &&
- !$.contains( this.$element[ 0 ], e.target ) &&
- ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
+ !OO.ui.contains( this.$element.add( this.$autoCloseIgnore ).get(), e.target, true )
) {
this.toggle( false );
}
this.popup = new OO.ui.PopupWidget( $.extend(
{ autoClose: true },
config.popup,
- { $autoCloseIgnore: this.$element }
+ { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) }
) );
};
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, div;
+ var hasInputWidget, $div;
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
icon: 'info'
} );
- div = $( '<div>' );
+ $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- div.html( config.help.toString() );
+ $div.html( config.help.toString() );
} else {
- div.text( config.help );
+ $div.text( config.help );
}
this.popupButtonWidget.getPopup().$body.append(
- div.addClass( 'oo-ui-fieldLayout-help-content' )
+ $div.addClass( 'oo-ui-fieldLayout-help-content' )
);
this.$help = this.popupButtonWidget.$element;
} else {
* @constructor
* @param {Object} [config] Configuration options
* @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
+ * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
+ * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
+ * For important messages, you are advised to use `notices`, as they are always shown.
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+ var $div;
+
// Configuration initialization
config = config || {};
icon: 'info'
} );
+ $div = $( '<div>' );
+ if ( config.help instanceof OO.ui.HtmlSnippet ) {
+ $div.html( config.help.toString() );
+ } else {
+ $div.text( config.help );
+ }
this.popupButtonWidget.getPopup().$body.append(
- $( '<div>' )
- .text( config.help )
- .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
);
this.$help = this.popupButtonWidget.$element;
} else {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-popupTool .oo-ui-popupWidget-popup,
.oo-ui-popupTool .oo-ui-popupWidget-anchor {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-tool.oo-ui-widget-enabled {
-webkit-transition: background-color 100ms;
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-iconElement-icon {
position: absolute;
}
+.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :-moz-placeholder {
+ color: #72777d;
+ opacity: 1;
+}
+.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content ::-moz-placeholder {
+ color: #72777d;
+ opacity: 1;
+}
+.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :-ms-input-placeholder {
+ color: #72777d;
+}
+.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content ::-webkit-input-placeholder {
+ color: #72777d;
+}
+.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :placeholder-shown {
+ color: #72777d;
+}
.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input {
border: 0;
line-height: 1.675;
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
setTimeout( function () {
if (
widget.isVisible() &&
- !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
- ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
+ !OO.ui.contains( widget.$element.add( widget.$autoCloseIgnore ).get(), document.activeElement, true )
) {
widget.toggle( false );
}
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-actionWidget.oo-ui-pendingElement-pending {
background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:42Z
+ * Date: 2016-11-29T22:57:42Z
*/
.oo-ui-window {
background: transparent;
/*!
- * OOjs UI v0.18.0
+ * OOjs UI v0.18.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-09T00:52:37Z
+ * Date: 2016-11-29T22:57:37Z
*/
( function ( OO ) {
*
* @param {OO.ui.Window|string} win Window object or symbolic name of window to open
* @param {Object} [data] Window opening data
- * @param {jQuery} [data.$returnFocusTo] Element to which the window will return focus when closed.
+ * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed.
+ * Defaults the current activeElement. If set to null, focus isn't changed on close.
* @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
* See {@link #event-opening 'opening' event} for more information about `opening` promises.
* @fires opening
manager.toggleGlobalEvents( false );
manager.toggleAriaIsolation( false );
}
- manager.$returnFocusTo[ 0 ].focus();
+ if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
+ manager.$returnFocusTo[ 0 ].focus();
+ }
manager.closing = null;
manager.currentWindow = null;
closing.resolve( data );
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M12 4c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm5 9H7v-2h10v2z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<g id="cancel">
<path id="circle-with-strike" d="M12 5.022a6.98 6.98 0 0 0-.003 13.956 6.98 6.98 0 0 0-.002-13.956zM6.885 12c0-1.092.572-3.25.93-2.93l7.113 7.114c.487.525-1.838.93-2.93.93A5.113 5.113 0 0 1 6.884 12zm9.298 2.93L9.07 7.815c-.445-.483 1.837-.93 2.93-.93a5.112 5.112 0 0 1 5.114 5.113c0 1.092-.364 3.542-.93 2.93z"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M17 7.5L9.5 15 6 11.5 4.5 13l5 5L20 7.5c-.706-.706-2.294-.706-3 0z" id="check"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M15 8s0-3-2.5-3S10 8 10 8v1h5zm2 0v1h2v10H9c-1.7 0-3-1.3-3-3V9h2V8s0-5 4.5-5S17 8 17 8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M10 8s0-3 2.5-3S15 8 15 8v1h-5zM8 8v1H6v10h10c1.7 0 3-1.3 3-3V9h-2V8s0-5-4.5-5S8 8 8 8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M18.748 11.717a1 1 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.413 0l-6.01-6.01c-.39-.382-.707-1.15-.707-1.7V6c0-.55.45-1 1-1h4.363c.55 0 1.32.318 1.71.707l6.01 6.01zM8.104 7.457a1.477 1.477 0 0 0 0 2.092 1.49 1.49 0 0 0 2.094 0 1.49 1.49 0 0 0 0-2.1 1.484 1.484 0 0 0-2.094 0z" id="tag"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M6 8c0-1.1.9-2 2-2h2l1-1h2l1 1h2c1.1 0 2 .9 2 2H6zm1 1h10l-1 11H8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M12 9V7s0-5-4.5-5S3 7 3 7h2s0-3 2.5-3S10 7 10 7v2H7v7c0 1.7 1.3 3 3 3h10V9z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M11 9V7s0-5 4.5-5S20 7 20 7h-2s0-3-2.5-3S13 7 13 7v2h3v7c0 1.7-1.3 3-3 3H3V9z"/>
</svg>
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
return valueParts.join( options.decimal );
}
+ /**
+ * Helper function to flip transformation tables.
+ *
+ * @param {...Object} Transformation tables
+ * @return {Object}
+ */
+ function flipTransform() {
+ var i, key, table, flipped = {};
+
+ // Ensure we strip thousand separators. This might be overwritten.
+ flipped[ ',' ] = '';
+
+ for ( i = 0; i < arguments.length; i++ ) {
+ table = arguments[ i ];
+ for ( key in table ) {
+ if ( table.hasOwnProperty( key ) ) {
+ // The thousand separator should be deleted
+ flipped[ table[ key ] ] = key === ',' ? '' : key;
+ }
+ }
+ }
+
+ return flipped;
+ }
+
$.extend( mw.language, {
/**
* @return {number|string} Formatted number
*/
convertNumber: function ( num, integer ) {
- var i, tmp, transformTable, numberString, convertedNumber, pattern;
-
- pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
- 'digitGroupingPattern' ) || '#,##0.###';
+ var transformTable, digitTransformTable, separatorTransformTable,
+ i, numberString, convertedNumber, pattern;
- // Set the target transform table:
- transformTable = mw.language.getDigitTransformTable();
-
- if ( !transformTable ) {
+ // Quick shortcut for plain numbers
+ if ( integer && parseInt( num, 10 ) === num ) {
return num;
}
- // Check if the 'restore' to Latin number flag is set:
+ // Load the transformation tables (can be empty)
+ digitTransformTable = mw.language.getDigitTransformTable();
+ separatorTransformTable = mw.language.getSeparatorTransformTable();
+
if ( integer ) {
- if ( parseInt( num, 10 ) === num ) {
- return num;
- }
- tmp = [];
- for ( i in transformTable ) {
- tmp[ transformTable[ i ] ] = i;
- }
- transformTable = tmp;
+ // Reverse the digit transformation tables if we are doing unformatting
+ transformTable = flipTransform( separatorTransformTable, digitTransformTable );
numberString = String( num );
} else {
- // Ignore transform table if wgTranslateNumerals is false
- if ( !mw.config.get( 'wgTranslateNumerals' ) ) {
- transformTable = [];
+ // This check being here means that digits can still be unformatted
+ // even if we do not produce them. This seems sane behavior.
+ if ( mw.config.get( 'wgTranslateNumerals' ) ) {
+ transformTable = digitTransformTable;
}
+
+ // Commaying is more complex, so we handle it here separately.
+ // When unformatting, we just use separatorTransformTable.
+ pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'digitGroupingPattern' ) || '#,##0.###';
numberString = mw.language.commafy( num, pattern );
}
convertedNumber = '';
for ( i = 0; i < numberString.length; i++ ) {
- if ( transformTable[ numberString[ i ] ] ) {
+ if ( transformTable.hasOwnProperty( numberString[ i ] ) ) {
convertedNumber += transformTable[ numberString[ i ] ];
} else {
convertedNumber += numberString[ i ];
}
}
- return integer ? parseInt( convertedNumber, 10 ) : convertedNumber;
+
+ if ( integer ) {
+ // Parse string to integer. This loses decimals!
+ convertedNumber = parseInt( convertedNumber, 10 );
+ }
+
+ return convertedNumber;
},
/**
--- /dev/null
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use IDatabase;
+use LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\ConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\ConnectionManager
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ */
+class ConnectionManagerTest extends \PHPUnit_Framework_TestCase {
+
+ /**
+ * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getIDatabaseMock() {
+ return $this->getMock( IDatabase::class );
+ }
+
+ /**
+ * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getLoadBalancerMock() {
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $lb;
+ }
+
+ public function testGetReadConnection_nullGroups() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getReadConnection();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetReadConnection_withGroups() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getReadConnection( [ 'group2' ] );
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetWriteConnection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getWriteConnection();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testReleaseConnection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new ConnectionManager( $lb );
+ $manager->releaseConnection( $database );
+ }
+
+ public function testGetReadConnectionRef_nullGroups() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnectionRef' )
+ ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getReadConnectionRef();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetReadConnectionRef_withGroups() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnectionRef' )
+ ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getReadConnectionRef( [ 'group2' ] );
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetWriteConnectionRef() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnectionRef' )
+ ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+ $actual = $manager->getWriteConnectionRef();
+
+ $this->assertSame( $database, $actual );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use IDatabase;
+use LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\SessionConsistentConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\SessionConsistentConnectionManager
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ */
+class ConsistentReadConnectionManagerTest extends \PHPUnit_Framework_TestCase {
+
+ /**
+ * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getIDatabaseMock() {
+ return $this->getMock( IDatabase::class );
+ }
+
+ /**
+ * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getLoadBalancerMock() {
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $lb;
+ }
+
+ public function testGetReadConnection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_REPLICA )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $actual = $manager->getReadConnection();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetReadConnectionReturnsWriteDbOnForceMatser() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->prepareForUpdates();
+ $actual = $manager->getReadConnection();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testGetWriteConnection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $actual = $manager->getWriteConnection();
+
+ $this->assertSame( $database, $actual );
+ }
+
+ public function testForceMaster() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER )
+ ->will( $this->returnValue( $database ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->prepareForUpdates();
+ $manager->getReadConnection();
+ }
+
+ public function testReleaseConnection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->releaseConnection( $database );
+ }
+
+ public function testBeginAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->exactly( 2 ) )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER )
+ ->will( $this->returnValue( $database ) );
+
+ $database->expects( $this->once() )
+ ->method( 'startAtomic' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->beginAtomicSection( 'TEST' );
+
+ // Should also ask for a DB_MASTER connection.
+ // This is asserted by the $lb mock.
+ $manager->getReadConnection();
+ }
+
+ public function testCommitAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $database->expects( $this->once() )
+ ->method( 'endAtomic' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->commitAtomicSection( $database, 'TEST' );
+ }
+
+ public function testRollbackAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $database->expects( $this->once() )
+ ->method( 'rollback' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->rollbackAtomicSection( $database, 'TEST' );
+ }
+
+}
},
{
lang: 'hi',
- number: '१२३४५६,७८९',
+ number: '१,२३,४५६',
result: '123456',
integer: true,
description: 'formatnum test for Hindi, Devanagari digits passed to get integer value'
assert.equal( mw.language.commafy( 123456789.567, '###,###,#0.00' ), '1,234,567,89.56', 'Decimal part as group of 3 and last one 2' );
} );
+ QUnit.test( 'mw.language.convertNumber', 2, function ( assert ) {
+ mw.language.setData( 'en', 'digitGroupingPattern', null );
+ mw.language.setData( 'en', 'digitTransformTable', null );
+ mw.language.setData( 'en', 'separatorTransformTable', { ',': '.', '.': ',' } );
+ mw.config.set( 'wgUserLanguage', 'en' );
+
+ assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting' );
+ assert.equal( mw.language.convertNumber( "1.800", true ), '1800', 'unformatting' );
+ } );
+
function grammarTest( langCode, test ) {
// The test works only if the content language is opt.language
// because it requires [lang].js to be loaded.