* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported for
performance reasons, and installations with this setting will now work as if it
was configured with 'any'.
+* (T185753) MediaWiki now defaults to using RemexHtml to tidy up user input, rather than
+ being off by default. If you wish to disable HTML tidying entirely, set $wgTidyConfig
+ to null; if you wish to use the old, deprecated Tidy external binary, both
+ set $wgTidyConfig to null and also set $wgUseTidy to true.
* $wgLogAutopatrol now defaults to false instead of true.
* $wgValidateAllHtml was removed and will be ignored.
/**
* Configuration for HTML postprocessing tool. Set this to a configuration
- * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically
- * used. See https://www.w3.org/People/Raggett/tidy/
+ * array to enable an external tool. By default, we now use the RemexHtml
+ * library; historically, Dave Raggett's "HTML Tidy" was typically used.
+ * See https://www.w3.org/People/Raggett/tidy/
*
* If this is null and $wgUseTidy is true, the deprecated configuration
* parameters will be used instead.
* - tidyBin: For RaggettExternal, the path to the tidy binary.
* - tidyCommandLine: For RaggettExternal, additional command line options.
*/
-$wgTidyConfig = null;
+$wgTidyConfig = [ 'driver' => 'RemexHtml' ];
/**
* Set this to true to use the deprecated tidy configuration parameters.
. "{$otherInfo['minSupported']}, you are using {$phpInfo['implementation']} "
. "{$phpInfo['version']}.";
- $longText = "Error: You might be using an older {$phpInfo['implementation']} version. \n"
+ $longText = "Error: You might be using an older {$phpInfo['implementation']} version "
+ . "({$phpInfo['implementation']} {$phpInfo['version']}). \n"
. "MediaWiki $this->mwVersion needs {$phpInfo['implementation']}"
. " $minimumVersion or higher or {$otherInfo['implementation']} version "
. "{$otherInfo['minSupported']}.\n\nCheck if you have a"
// RACE: $name was already in the db, probably just inserted, so load from master
// Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs
$table = $this->loadTable(
- $this->getDBConnection( DB_MASTER, LoadBalancer::CONN_TRX_AUTO )
+ $this->getDBConnection( DB_MASTER, LoadBalancer::CONN_TRX_AUTOCOMMIT )
);
$searchResult = array_search( $name, $table, true );
if ( $searchResult === false ) {
if ( isset( $show['unpatrolled'] ) ) {
// See ChangesList::isUnpatrolled
if ( $user->useRCPatrol() ) {
- $this->addWhere( 'rc_patrolled = 0' );
+ $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
} elseif ( $user->useNPPatrol() ) {
- $this->addWhere( 'rc_patrolled = 0' );
+ $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
$this->addWhereFld( 'rc_type', RC_NEW );
}
}
- $this->addWhereIf( 'rc_patrolled != 2', isset( $show['!autopatrolled'] ) );
- $this->addWhereIf( 'rc_patrolled = 2', isset( $show['autopatrolled'] ) );
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['!autopatrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['autopatrolled'] )
+ );
// Don't throw log entries out the window here
$this->addWhereIf(
/* Add the patrolled flag */
if ( $this->fld_patrolled ) {
- $vals['patrolled'] = $row->rc_patrolled != 0;
+ $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
$vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
- $vals['autopatrolled'] = $row->rc_patrolled == 2;
+ $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
}
if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
$this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
$this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
- $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
- $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
- $this->addWhereIf( 'rc_patrolled != 2', isset( $show['!autopatrolled'] ) );
- $this->addWhereIf( 'rc_patrolled = 2', isset( $show['autopatrolled'] ) );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED,
+ isset( $show['!patrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED,
+ isset( $show['patrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['!autopatrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['autopatrolled'] )
+ );
$this->addWhereIf( $idField . ' != page_latest', isset( $show['!top'] ) );
$this->addWhereIf( $idField . ' = page_latest', isset( $show['top'] ) );
$this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
/* Add the patrolled flag */
if ( $this->fld_patrol ) {
- $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != 0;
+ $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != RecentChange::PRC_UNPATROLLED;
$vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
- $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == 2;
+ $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == RecentChange::PRC_AUTOPATROLLED;
}
if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
$params = $this->extractRequestParams();
// Figure out expiry times from the input
- // $params['expiry'] may not be set in subclasses
+ // $params['expiry'] is not set in CentralAuth's ApiGlobalUserRights subclass
if ( isset( $params['expiry'] ) ) {
$expiry = (array)$params['expiry'];
} else {
$expiry = [ 'infinity' ];
}
$add = (array)$params['add'];
- if ( count( $expiry ) !== count( $add ) ) {
+ if ( !$add ) {
+ $expiry = [];
+ } elseif ( count( $expiry ) !== count( $add ) ) {
if ( count( $expiry ) === 1 ) {
$expiry = array_fill( 0, count( $add ), $expiry[0] );
} else {
ApiBase::PARAM_ISMULTI => true
],
];
+ // CentralAuth's ApiGlobalUserRights subclass can't handle expiries
if ( !$this->getUserRightsPage()->canProcessExpiries() ) {
unset( $a['expiry'] );
}
$dbw->update(
'recentchanges',
[
- 'rc_patrolled' => 1
+ 'rc_patrolled' => self::PRC_PATROLLED
],
[
'rc_id' => $this->getAttribute( 'rc_id' )
'rc_last_oldid' => 0,
'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => $markPatrolled ? 1 : 0,
+ 'rc_patrolled' => $markPatrolled ? self::PRC_PATROLLED : self::PRC_UNPATROLLED,
'rc_new' => 0, # obsolete
'rc_old_len' => null,
'rc_new_len' => null,
'rc_last_oldid' => $oldRevId,
'rc_bot' => $bot ? 1 : 0,
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => 1, // Always patrolled, just like log entries
+ 'rc_patrolled' => self::PRC_PATROLLED, // Always patrolled, just like log entries
'rc_new' => 0, # obsolete
'rc_old_len' => null,
'rc_new_len' => null,
[
'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
'rc_this_oldid' => $this->mNewid,
- 'rc_patrolled' => 0
+ 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
],
__METHOD__
);
*/
protected $mCaption = false;
+ /**
+ * Length to truncate filename to in caption when using "showfilename".
+ * A value of 'true' will truncate the filename to one line using CSS
+ * and will be the behaviour after deprecation.
+ *
+ * @var bool|int
+ */
+ protected $mCaptionLength = true;
+
/**
* @var bool Hide blacklisted images?
*/
Linker::linkKnown(
$nt,
htmlspecialchars(
- $this->mCaptionLength !== true ?
- $lang->truncate( $nt->getText(), $this->mCaptionLength ) :
+ is_int( $this->getCaptionLength() ) ?
+ $lang->truncate( $nt->getText(), $this->getCaptionLength() ) :
$nt->getText()
),
[
'class' => 'galleryfilename' .
- ( $this->mCaptionLength === true ? ' galleryfilename-truncate' : '' )
+ ( $this->getCaptionLength() === true ? ' galleryfilename-truncate' : '' )
]
) . "\n" :
'';
$galleryText = $textlink . $text . $meta;
$galleryText = $this->wrapGalleryText( $galleryText, $thumb );
+ $gbWidth = $this->getGBWidth( $thumb ) . 'px';
+ if ( $this->getGBWidthOverwrite( $thumb ) ) {
+ $gbWidth = $this->getGBWidthOverwrite( $thumb );
+ }
# Weird double wrapping (the extra div inside the li) needed due to FF2 bug
# Can be safely removed if FF2 falls completely out of existence
$output .= "\n\t\t" . '<li class="gallerybox" style="width: '
- . $this->getGBWidth( $thumb ) . 'px">'
- . '<div style="width: ' . $this->getGBWidth( $thumb ) . 'px">'
+ . $gbWidth . '">'
+ . '<div style="width: ' . $gbWidth . '">'
. $thumbhtml
. $galleryText
. "\n\t\t</div></li>";
return 8;
}
+ /**
+ * Length to truncate filename to in caption when using "showfilename" (if int).
+ * A value of 'true' will truncate the filename to one line using CSS, while
+ * 'false' will disable truncating.
+ *
+ * @return int|bool
+ */
+ protected function getCaptionLength() {
+ return $this->mCaptionLength;
+ }
+
/**
* Get total padding.
*
}
/**
- * Width of gallerybox <li>.
+ * Computed width of gallerybox <li>.
*
* Generally is the width of the image, plus padding on image
* plus padding on gallerybox.
return $this->mWidths + $this->getThumbPadding() + $this->getGBPadding();
}
+ /**
+ * Allows overwriting the computed width of the gallerybox <li> with a string,
+ * like '100%'.
+ *
+ * Generally is the width of the image, plus padding on image
+ * plus padding on gallerybox.
+ *
+ * @note Important: parameter will be false if no thumb used.
+ * @param MediaTransformOutput|bool $thumb MediaTransformObject object or false.
+ * @return bool|string Ignored if false.
+ */
+ protected function getGBWidthOverwrite( $thumb ) {
+ return false;
+ }
+
/**
* Get a list of modules to include in the page.
*
// If the connection is busy with a transaction, then defer the job writes
// until right before the main round commit step. Any errors that bubble
// up will rollback the main commit round.
- // b) mysql/postgres; DB connection is generally a separate CONN_TRX_AUTO handle.
+ // b) mysql/postgres; DB connection is generally a separate CONN_TRX_AUTOCOMMIT handle.
// No transaction is active nor will be started by writes, so enqueue the jobs
// now so that any errors will show up immediately as the interface expects. Any
// errors that bubble up will rollback the main commit round.
return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
// Keep a separate connection to avoid contention and deadlocks;
// However, SQLite has the opposite behavior due to DB-level locking.
- ? $lb->getConnectionRef( $index, [], $this->wiki, $lb::CONN_TRX_AUTO )
+ ? $lb->getConnectionRef( $index, [], $this->wiki, $lb::CONN_TRX_AUTOCOMMIT )
// Jobs insertion will be defered until the PRESEND stage to reduce contention.
: $lb->getConnectionRef( $index, [], $this->wiki );
}
/**
* Serialize a string (escape and quote) for use as a CSS string value.
- * http://www.w3.org/TR/2013/WD-cssom-20131205/#serialize-a-string
+ * https://www.w3.org/TR/2016/WD-cssom-1-20160317/#serialize-a-string
*
* @param string $value
* @return string
- * @throws Exception
*/
public static function serializeStringValue( $value ) {
- if ( strstr( $value, "\0" ) ) {
- throw new Exception( "Invalid character in CSS string" );
- }
- $value = strtr( $value, [ '\\' => '\\\\', '"' => '\\"' ] );
- $value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
+ $value = strtr( $value, [ "\0" => "\\fffd ", '\\' => '\\\\', '"' => '\\"' ] );
+ $value = preg_replace_callback( '/[\x01-\x1f\x7f]/', function ( $match ) {
return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
}, $value );
return '"' . $value . '"';
const DOMAIN_ANY = '';
/** @var int DB handle should have DBO_TRX disabled and the caller will leave it as such */
+ const CONN_TRX_AUTOCOMMIT = 1;
+ /** @var int Alias for CONN_TRX_AUTOCOMMIT for b/c; deprecated since 1.31 */
const CONN_TRX_AUTO = 1;
/**
/**
* Get a connection handle by server index
*
- * The CONN_TRX_AUTO flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
* (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
* can be used to check such flags beforehand.
*
- * If the caller uses $domain or sets CONN_TRX_AUTO in $flags, then it must also
+ * If the caller uses $domain or sets CONN_TRX_AUTOCOMMIT in $flags, then it must also
* call ILoadBalancer::reuseConnection() on the handle when finished using it.
* In all other cases, this is not necessary, though not harmful either.
*
*
* The handle's methods simply wrap those of a Database handle
*
- * The CONN_TRX_AUTO flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
* (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
* can be used to check such flags beforehand.
*
* @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return DBConnRef
*/
public function getConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
*
* The handle's methods simply wrap those of a Database handle
*
- * The CONN_TRX_AUTO flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
* (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
* can be used to check such flags beforehand.
*
* @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return DBConnRef
*/
public function getLazyConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
*
* The handle's methods simply wrap those of a Database handle
*
- * The CONN_TRX_AUTO flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
* (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
* can be used to check such flags beforehand.
*
* @param int $db Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return MaintainableDBConnRef
*/
public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false, $flags = 0 );
* The index must be an actual index into the array. If a connection to the server is
* already open and not considered an "in use" foreign connection, this simply returns it.
*
- * Avoid using CONN_TRX_AUTO for databases with ATTR_DB_LEVEL_LOCKING (e.g. sqlite) in
+ * Avoid using CONN_TRX_AUTOCOMMIT for databases with ATTR_DB_LEVEL_LOCKING (e.g. sqlite) in
* order to avoid deadlocks. ILoadBalancer::getServerAttributes() can be used to check
* such flags beforehand.
*
- * If the caller uses $domain or sets CONN_TRX_AUTO in $flags, then it must also
+ * If the caller uses $domain or sets CONN_TRX_AUTOCOMMIT in $flags, then it must also
* call ILoadBalancer::reuseConnection() on the handle when finished using it.
* In all other cases, this is not necessary, though not harmful either.
*
*
* @param int $i Server index (does not support DB_MASTER/DB_REPLICA)
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return Database|bool Returns false on errors
* @throws DBAccessError
*/
if ( !empty( $connsByServer[$i] ) ) {
/** @var IDatabase[] $serverConns */
$serverConns = $connsByServer[$i];
-
return reset( $serverConns );
}
}
$domain = false; // local connection requested
}
- if ( ( $flags & self::CONN_TRX_AUTO ) === self::CONN_TRX_AUTO ) {
+ if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) === self::CONN_TRX_AUTOCOMMIT ) {
// Assuming all servers are of the same type (or similar), which is overwhelmingly
// the case, use the master server information to get the attributes. The information
// for $i cannot be used since it might be DB_REPLICA, which might require connection
// rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
// to reduce lock contention. None of these apply for sqlite and using separate
// connections just causes self-deadlocks.
- $flags &= ~self::CONN_TRX_AUTO;
- $this->connLogger->info( __METHOD__ . ': ignoring CONN_TRX_AUTO to avoid deadlocks.' );
+ $flags &= ~self::CONN_TRX_AUTOCOMMIT;
+ $this->connLogger->info( __METHOD__ .
+ ': ignoring CONN_TRX_AUTOCOMMIT to avoid deadlocks.' );
}
}
// main set of DB connections but rather its own pool since:
// a) those are usually set to implicitly use transaction rounds via DBO_TRX
// b) those must support the use of explicit transaction rounds via beginMasterChanges()
- $autoCommit = ( ( $flags & self::CONN_TRX_AUTO ) == self::CONN_TRX_AUTO );
+ $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
if ( $domain !== false ) {
// Connection is to a foreign domain
$domainInstance = DatabaseDomain::newFromId( $domain );
$dbName = $domainInstance->getDatabase();
$prefix = $domainInstance->getTablePrefix();
- $autoCommit = ( ( $flags & self::CONN_TRX_AUTO ) == self::CONN_TRX_AUTO );
+ $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
if ( $autoCommit ) {
$connFreeKey = self::KEY_FOREIGN_FREE_NOROUND;
}
public function closeConnection( IDatabase $conn ) {
- $serverIndex = $conn->getLBInfo( 'serverIndex' ); // second index level of mConns
+ $serverIndex = $conn->getLBInfo( 'serverIndex' );
foreach ( $this->conns as $type => $connsByServer ) {
if ( !isset( $connsByServer[$serverIndex] ) ) {
continue;
// Log the autopatrol if the log entry is patrollable
if ( $this->getIsPatrollable() &&
- $rc->getAttribute( 'rc_patrolled' ) === 2
+ $rc->getAttribute( 'rc_patrolled' ) === RecentChange::PRC_AUTOPATROLLED
) {
PatrolLog::record( $rc, true, $this->getPerformer() );
}
$index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
// Keep a separate connection to avoid contention and deadlocks
- $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTO );
+ $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
// @TODO: Use a blank trx profiler to ignore expections as this is a cache
} else {
// However, SQLite has the opposite behavior due to DB-level locking.
if ( $wgUseRCPatrol ) {
// Mark all reverted edits as patrolled
- $set['rc_patrolled'] = 1;
+ $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
}
if ( count( $set ) ) {
}
define( 'MW_DB', $bits[0] );
define( 'MW_PREFIX', $bits[1] );
+ } elseif ( isset( $this->mOptions['server'] ) ) {
+ // Provide the option for site admins to detect and configure
+ // multiple wikis based on server names. This offers --server
+ // as alternative to --wiki.
+ // See https://www.mediawiki.org/wiki/Manual:Wiki_family
+ $_SERVER['SERVER_NAME'] = $this->mOptions['server'];
}
if ( !is_readable( $settingsFile ) ) {
"must exist and be readable in the source directory.\n" .
"Use --conf to specify it." );
}
- if ( isset( $this->mOptions['server'] ) ) {
- $_SERVER['SERVER_NAME'] = $this->mOptions['server'];
- }
$wgCommandLineMode = true;
return $settingsFile;
'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php",
+ 'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php",
# tests/phpunit/includes
'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
--- /dev/null
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+trait HamcrestPHPUnitIntegration {
+
+ /**
+ * Wrapper around Hamcrest's assertThat, which marks the assertion
+ * for PHPUnit so the test is not marked as risky
+ */
+ public function assertThatHamcrest( /* ... */ ) {
+ call_user_func_array( 'assertThat', func_get_args() );
+ $this->addToAssertionCount( 1 );
+ }
+}
MediaWikiServices::forceGlobalInstance( $oldServices );
$newServices->destroy();
+
+ // No exception was thrown, avoid being risky
+ $this->assertTrue( true );
}
public function testResetChildProcessServices() {
public static function clear() {
self::$testUsers = [];
}
+
+ /**
+ * @todo It would be nice if this were a non-static method of TestUser
+ * instead, but that doesn't seem possible without friends?
+ *
+ * @return bool True if it's safe to modify the user
+ */
+ public static function isMutable( User $user ) {
+ foreach ( self::$testUsers as $key => $testUser ) {
+ if ( $user === $testUser->getUser() ) {
+ return false;
+ }
+ }
+ return true;
+ }
}
}
if ( $tokenType !== null ) {
+ if ( $tokenType === 'auto' ) {
+ $tokenType = ( new ApiMain() )->getModuleManager()
+ ->getModule( $params['action'], 'action' )->needsToken();
+ }
$params['token'] = ApiQueryTokens::getToken(
$wgUser, $sessionObj, ApiQueryTokens::getTokenTypeSalts()[$tokenType]
)->toString();
* @return array Result of the API call
*/
protected function doApiRequestWithToken( array $params, array $session = null,
- User $user = null, $tokenType = 'csrf'
+ User $user = null, $tokenType = 'auto'
) {
return $this->doApiRequest( $params, $session, false, $user, $tokenType );
}
--- /dev/null
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiUserrights
+ */
+class ApiUserrightsTest extends ApiTestCase {
+ /**
+ * Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets
+ * $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the
+ * specified values.
+ *
+ * @param array|bool $add Groups bureaucrats should be allowed to add, true for all
+ * @param array|bool $remove Groups bureaucrats should be allowed to remove, true for all
+ */
+ protected function setPermissions( $add = [], $remove = [] ) {
+ global $wgAddGroups, $wgRemoveGroups;
+
+ $this->setGroupPermissions( 'bureaucrat', 'userrights', false );
+
+ if ( $add ) {
+ $this->stashMwGlobals( 'wgAddGroups' );
+ $wgAddGroups['bureaucrat'] = $add;
+ }
+ if ( $remove ) {
+ $this->stashMwGlobals( 'wgRemoveGroups' );
+ $wgRemoveGroups['bureaucrat'] = $remove;
+ }
+ }
+
+ /**
+ * Perform an API userrights request that's expected to be successful.
+ *
+ * @param array|string $expectedGroups Group(s) that the user is expected
+ * to have after the API request
+ * @param array $params Array to pass to doApiRequestWithToken(). 'action'
+ * => 'userrights' is implicit. If no 'user' or 'userid' is specified,
+ * we add a 'user' parameter. If no 'add' or 'remove' is specified, we
+ * add 'add' => 'sysop'.
+ * @param User|null $user The user that we're modifying. The user must be
+ * mutable, because we're going to change its groups! null means that
+ * we'll make up our own user to modify, and doesn't make sense if 'user'
+ * or 'userid' is specified in $params.
+ */
+ protected function doSuccessfulRightsChange(
+ $expectedGroups = 'sysop', array $params = [], User $user = null
+ ) {
+ $expectedGroups = (array)$expectedGroups;
+ $params['action'] = 'userrights';
+
+ if ( !$user ) {
+ $user = $this->getMutableTestUser()->getUser();
+ }
+
+ $this->assertTrue( TestUserRegistry::isMutable( $user ),
+ 'Immutable user passed to doSuccessfulRightsChange!' );
+
+ if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
+ $params['user'] = $user->getName();
+ }
+ if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
+ $params['add'] = 'sysop';
+ }
+
+ $res = $this->doApiRequestWithToken( $params );
+
+ $user->clearInstanceCache();
+ $this->assertSame( $expectedGroups, $user->getGroups() );
+
+ $this->assertArrayNotHasKey( 'warnings', $res[0] );
+ }
+
+ /**
+ * Perform an API userrights request that's expected to fail.
+ *
+ * @param string $expectedException Expected exception text
+ * @param array $params As for doSuccessfulRightsChange()
+ * @param User|null $user As for doSuccessfulRightsChange(). If there's no
+ * user who will possibly be affected (such as if an invalid username is
+ * provided in $params), pass null.
+ */
+ protected function doFailedRightsChange(
+ $expectedException, array $params = [], User $user = null
+ ) {
+ $params['action'] = 'userrights';
+
+ $this->setExpectedException( ApiUsageException::class, $expectedException );
+
+ if ( !$user ) {
+ // If 'user' or 'userid' is specified and $user was not specified,
+ // the user we're creating now will have nothing to do with the API
+ // request, but that's okay, since we're just testing that it has
+ // no groups.
+ $user = $this->getMutableTestUser()->getUser();
+ }
+
+ $this->assertTrue( TestUserRegistry::isMutable( $user ),
+ 'Immutable user passed to doFailedRightsChange!' );
+
+ if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
+ $params['user'] = $user->getName();
+ }
+ if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
+ $params['add'] = 'sysop';
+ }
+ $expectedGroups = $user->getGroups();
+
+ try {
+ $this->doApiRequestWithToken( $params );
+ } finally {
+ $user->clearInstanceCache();
+ $this->assertSame( $expectedGroups, $user->getGroups() );
+ }
+ }
+
+ public function testAdd() {
+ $this->doSuccessfulRightsChange();
+ }
+
+ public function testBlockedWithUserrights() {
+ global $wgUser;
+
+ $block = new Block( [ 'address' => $wgUser, 'by' => $wgUser->getId(), ] );
+ $block->insert();
+
+ try {
+ $this->doSuccessfulRightsChange();
+ } finally {
+ $block->delete();
+ $wgUser->clearInstanceCache();
+ }
+ }
+
+ public function testBlockedWithoutUserrights() {
+ $user = $this->getTestSysop()->getUser();
+
+ $this->setPermissions( true, true );
+
+ $block = new Block( [ 'address' => $user, 'by' => $user->getId() ] );
+ $block->insert();
+
+ try {
+ $this->doFailedRightsChange( 'You have been blocked from editing.' );
+ } finally {
+ $block->delete();
+ $user->clearInstanceCache();
+ }
+ }
+
+ public function testAddMultiple() {
+ $this->doSuccessfulRightsChange(
+ [ 'bureaucrat', 'sysop' ],
+ [ 'add' => 'bureaucrat|sysop' ]
+ );
+ }
+
+ public function testTooFewExpiries() {
+ $this->doFailedRightsChange(
+ '2 expiry timestamps were provided where 3 were needed.',
+ [ 'add' => 'sysop|bureaucrat|bot', 'expiry' => 'infinity|tomorrow' ]
+ );
+ }
+
+ public function testTooManyExpiries() {
+ $this->doFailedRightsChange(
+ '3 expiry timestamps were provided where 2 were needed.',
+ [ 'add' => 'sysop|bureaucrat', 'expiry' => 'infinity|tomorrow|never' ]
+ );
+ }
+
+ public function testInvalidExpiry() {
+ $this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] );
+ }
+
+ public function testMultipleInvalidExpiries() {
+ $this->doFailedRightsChange(
+ 'Invalid expiry time "foo".',
+ [ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ]
+ );
+ }
+
+ public function testWithTag() {
+ ChangeTags::defineTag( 'custom tag' );
+
+ $user = $this->getMutableTestUser()->getUser();
+
+ $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $this->assertSame(
+ 'custom tag',
+ $dbr->selectField(
+ [ 'change_tag', 'logging' ],
+ 'ct_tag',
+ [
+ 'ct_log_id = log_id',
+ 'log_namespace' => NS_USER,
+ 'log_title' => strtr( $user->getName(), ' ', '_' )
+ ],
+ __METHOD__
+ )
+ );
+ }
+
+ public function testWithoutTagPermission() {
+ global $wgGroupPermissions;
+
+ ChangeTags::defineTag( 'custom tag' );
+
+ $this->stashMwGlobals( 'wgGroupPermissions' );
+ $wgGroupPermissions['user']['applychangetags'] = false;
+
+ $this->doFailedRightsChange(
+ 'You do not have permission to apply change tags along with your changes.',
+ [ 'tags' => 'custom tag' ]
+ );
+ }
+
+ public function testNonexistentUser() {
+ $this->doFailedRightsChange(
+ 'There is no user by the name "Nonexistent user". Check your spelling.',
+ [ 'user' => 'Nonexistent user' ]
+ );
+ }
+
+ public function testWebToken() {
+ $sysop = $this->getTestSysop()->getUser();
+ $user = $this->getMutableTestUser()->getUser();
+
+ $token = $sysop->getEditToken( $user->getName() );
+
+ $res = $this->doApiRequest( [
+ 'action' => 'userrights',
+ 'user' => $user->getName(),
+ 'add' => 'sysop',
+ 'token' => $token,
+ ] );
+
+ $user->clearInstanceCache();
+ $this->assertSame( [ 'sysop' ], $user->getGroups() );
+
+ $this->assertArrayNotHasKey( 'warnings', $res[0] );
+ }
+
+ /**
+ * Helper for testCanProcessExpiries that returns a mock ApiUserrights that either can or cannot
+ * process expiries. Although the regular page can process expiries, we use a mock here to
+ * ensure that it's the result of canProcessExpiries() that makes a difference, and not some
+ * error in the way we construct the mock.
+ *
+ * @param bool $canProcessExpiries
+ */
+ private function getMockForProcessingExpiries( $canProcessExpiries ) {
+ $sysop = $this->getTestSysop()->getUser();
+ $user = $this->getMutableTestUser()->getUser();
+
+ $token = $sysop->getEditToken( 'userrights' );
+
+ $main = new ApiMain( new FauxRequest( [
+ 'action' => 'userrights',
+ 'user' => $user->getName(),
+ 'add' => 'sysop',
+ 'token' => $token,
+ ] ) );
+
+ $mockUserRightsPage = $this->getMockBuilder( UserrightsPage::class )
+ ->setMethods( [ 'canProcessExpiries' ] )
+ ->getMock();
+ $mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries );
+
+ $mockApi = $this->getMockBuilder( ApiUserrights::class )
+ ->setConstructorArgs( [ $main, 'userrights' ] )
+ ->setMethods( [ 'getUserRightsPage' ] )
+ ->getMock();
+ $mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage );
+
+ return $mockApi;
+ }
+
+ public function testCanProcessExpiries() {
+ $mock1 = $this->getMockForProcessingExpiries( true );
+ $this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() );
+
+ $mock2 = $this->getMockForProcessingExpiries( false );
+ $this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() );
+ }
+
+ /**
+ * Tests adding and removing various groups with various permissions.
+ *
+ * @dataProvider addAndRemoveGroupsProvider
+ * @param array|null $permissions [ [ $wgAddGroups, $wgRemoveGroups ] ] or null for 'userrights'
+ * to be set in $wgGroupPermissions
+ * @param array $groupsToChange [ [ groups to add ], [ groups to remove ] ]
+ * @param array $expectedGroups Array of expected groups
+ */
+ public function testAddAndRemoveGroups(
+ array $permissions = null, array $groupsToChange, array $expectedGroups
+ ) {
+ if ( $permissions !== null ) {
+ $this->setPermissions( $permissions[0], $permissions[1] );
+ }
+
+ $params = [
+ 'add' => implode( '|', $groupsToChange[0] ),
+ 'remove' => implode( '|', $groupsToChange[1] ),
+ ];
+
+ // We'll take a bot so we have a group to remove
+ $user = $this->getMutableTestUser( [ 'bot' ] )->getUser();
+
+ $this->doSuccessfulRightsChange( $expectedGroups, $params, $user );
+ }
+
+ public function addAndRemoveGroupsProvider() {
+ return [
+ 'Simple add' => [
+ [ [ 'sysop' ], [] ],
+ [ [ 'sysop' ], [] ],
+ [ 'bot', 'sysop' ]
+ ], 'Add with only remove permission' => [
+ [ [], [ 'sysop' ] ],
+ [ [ 'sysop' ], [] ],
+ [ 'bot' ],
+ ], 'Add with global remove permission' => [
+ [ [], true ],
+ [ [ 'sysop' ], [] ],
+ [ 'bot' ],
+ ], 'Simple remove' => [
+ [ [], [ 'bot' ] ],
+ [ [], [ 'bot' ] ],
+ [],
+ ], 'Remove with only add permission' => [
+ [ [ 'bot' ], [] ],
+ [ [], [ 'bot' ] ],
+ [ 'bot' ],
+ ], 'Remove with global add permission' => [
+ [ true, [] ],
+ [ [], [ 'bot' ] ],
+ [ 'bot' ],
+ ], 'Add and remove same new group' => [
+ null,
+ [ [ 'sysop' ], [ 'sysop' ] ],
+ // The userrights code does removals before adds, so it doesn't remove the sysop
+ // group here and only adds it.
+ [ 'bot', 'sysop' ],
+ ], 'Add and remove same existing group' => [
+ null,
+ [ [ 'bot' ], [ 'bot' ] ],
+ // But here it first removes the existing group and then re-adds it.
+ [ 'bot' ],
+ ],
+ ];
+ }
+}
);
$mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key ) );
- $mocks[$key . '2'] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
$mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key . '2' ) );
- $mocks[$key . '3'] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
$mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key . '3' ) );
}
) );
for ( $i = 2; $i <= 3; $i++ ) {
- $mocks[$key . $i] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
$mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key . $i ) );
$mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
$mocks = [];
foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
$class = ucfirst( $key ) . 'AuthenticationProvider';
- $mocks[$key] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
$mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key ) );
}
$mocks = [];
foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
$class = ucfirst( $key ) . 'AuthenticationProvider';
- $mocks[$key] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
+ ->setMethods( [
+ 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
$mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key ) );
$mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
PrimaryAuthenticationProvider::TYPE_LINK
] as $type ) {
$class = 'PrimaryAuthenticationProvider';
- $mocks["primary-$type"] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
+ ->setMethods( [
+ 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
+ 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
$mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( "primary-$type" ) );
$mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
$this->primaryauthMocks[] = $mocks["primary-$type"];
}
- $mocks['primary2'] = $this->getMockForAbstractClass(
- PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
- );
+ $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider::class )
+ ->setMethods( [
+ 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
+ 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
$mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( 'primary2' ) );
$mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
$mocks = [];
foreach ( [ 'primary', 'secondary' ] as $key ) {
$class = ucfirst( $key ) . 'AuthenticationProvider';
- $mocks[$key] = $this->getMockForAbstractClass(
- "MediaWiki\\Auth\\$class", [], "Mock$class"
- );
+ $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
$mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
->will( $this->returnValue( $key ) );
$mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
public function testAutoCreateFailOnLogin() {
$username = self::usernameForCreation();
- $mock = $this->getMockForAbstractClass(
- PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
$mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
$mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
$this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
- $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
- $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
+ $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertFalse(
+ $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
- $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTO uses separate connection" );
+ $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
- $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTO );
- $this->assertFalse( $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
+ $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertFalse(
+ $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
- $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTO uses separate connection" );
+ $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
- $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
- $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTO reuses connections" );
+ $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
$lb->closeAll();
}
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
$this->assertWriteForbidden( $dbr );
- $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
- $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
+ $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertFalse(
+ $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
- $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTO uses separate connection" );
+ $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
- $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTO );
- $this->assertFalse( $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
+ $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertFalse(
+ $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
- $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTO uses separate connection" );
+ $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
- $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
- $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTO reuses connections" );
+ $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
$lb->closeAll();
}
->method( 'send' )
->will( $this->returnValue( true ) );
// evil hax
- TestingAccessWrapper::newFromObject( $mockMethod )->matcher->parametersMatcher =
+ $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher;
+ TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher =
new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
[ $this->anything(), $this->anything(), [ 'words' ] ],
[ $this->anything(), $this->anything(), [ 'lines' ] ]
class JobQueueDBSingle extends JobQueueDB {
protected function getDB( $index ) {
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- // Override to not use CONN_TRX_AUTO so that we see the same temporary `job` table
+ // Override to not use CONN_TRX_AUTOCOMMIT so that we see the same temporary `job` table
return $lb->getConnection( $index, [], $this->wiki );
}
}
] );
}
+ /**
+ * @dataProvider serializeStringValueProvider
+ * @covers CSSMin::serializeStringValue
+ */
+ public function testSerializeStringValue( $input, $expected ) {
+ $output = CSSMin::serializeStringValue( $input );
+ $this->assertEquals(
+ $expected,
+ $output,
+ 'Serialized output must be in the expected form.'
+ );
+ }
+
+ public function serializeStringValueProvider() {
+ return [
+ [ 'Hello World!', '"Hello World!"' ],
+ [ "Null\0Null", "\"Null\\fffd Null\"" ],
+ [ '"', '"\\""' ],
+ [ "'", '"\'"' ],
+ [ "\\", '"\\\\"' ],
+ [ "Tab\tTab", '"Tab\\9 Tab"' ],
+ [ "Space tab \t space", '"Space tab \\9 space"' ],
+ [ "Line\nfeed", '"Line\\a feed"' ],
+ [ "Return\rreturn", '"Return\\d return"' ],
+ [ "Next\xc2\x85line", "\"Next\xc2\x85line\"" ],
+ [ "Del\x7fDel", '"Del\\7f Del"' ],
+ [ "nb\xc2\xa0sp", "\"nb\xc2\xa0sp\"" ],
+ [ "AMP&AMP", "\"AMP&AMP\"" ],
+ [ '!"#$%&\'()*+,-./0123456789:;<=>?', '"!\\"#$%&\'()*+,-./0123456789:;<=>?"' ],
+ [ '@[\\]^_`{|}~', '"@[\\\\]^_`{|}~"' ],
+ [ 'ä', '"ä"' ],
+ [ 'Ä', '"Ä"' ],
+ [ '€', '"€"' ],
+ [ '𝒞', '"𝒞"' ], // U+1D49E 'MATHEMATICAL SCRIPT CAPITAL C'
+ ];
+ }
+
/**
* @dataProvider mimeTypeProvider
* @covers CSSMin::getMimeType
$lb->expects( $this->once() )
->method( 'getConnection' )
- ->with( DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTO )
+ ->with( DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT )
->willReturnCallback(
function () {
return $this->getDatabaseMock();
$ref = new DBConnRef(
$lb,
- [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTO ]
+ [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ]
);
$this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
'<editsection> should survive tidy'
],
[ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ],
- [ "<link foo=\"bar\" />\nfoo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
- [ "<meta foo=\"bar\" />\nfoo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
+ [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
+ [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
[ $testMathML, $testMathML, '<math> should survive tidy' ],
];
}
class VersionCheckerTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
+ use PHPUnit4And6Compat;
/**
* @dataProvider provideCheck
class BatchRowUpdateTest extends MediaWikiTestCase {
public function testWriterBasicFunctionality() {
- $db = $this->mockDb();
+ $db = $this->mockDb( [ 'update' ] );
$writer = new BatchRowWriter( $db, 'echo_event' );
$updates = [
}
public function testReaderBasicIterate() {
- $db = $this->mockDb();
$batchSize = 2;
- $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize );
-
$response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () {
static $i = 0;
return [ 'id_field' => ++$i ];
} );
- $db->expects( $this->exactly( count( $response ) ) )
- ->method( 'select' )
- ->will( $this->consecutivelyReturnFromSelect( $response ) );
+ $db = $this->mockDbConsecutiveSelect( $response );
+ $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize );
$pos = 0;
foreach ( $reader as $rows ) {
public function testReaderSetFetchColumns(
$message, array $columns, array $primaryKeys, array $fetchColumns
) {
- $db = $this->mockDb();
+ $db = $this->mockDb( [ 'select' ] );
$db->expects( $this->once() )
->method( 'select' )
// only testing second parameter of Database::select
}
protected function mockDbConsecutiveSelect( array $retvals ) {
- $db = $this->mockDb();
+ $db = $this->mockDb( [ 'select', 'addQuotes' ] );
$db->expects( $this->any() )
->method( 'select' )
->will( $this->consecutivelyReturnFromSelect( $retvals ) );
return $res;
}
- protected function mockDb() {
+ protected function mockDb( $methods = [] ) {
// @TODO: mock from Database
// FIXME: the constructor normally sets mAtomicLevels and mSrvCache
$databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
->disableOriginalConstructor()
+ ->setMethods( array_merge( [ 'isOpen', 'getApproximateLagStatus' ], $methods ) )
->getMock();
$databaseMysql->expects( $this->any() )
->method( 'isOpen' )
private function getMockCache() {
$mock = $this->getMockBuilder( HashBagOStuff::class )
->disableOriginalConstructor()
+ ->setMethods( [ 'get', 'set', 'delete', 'makeKey' ] )
->getMock();
$mock->expects( $this->any() )
->method( 'makeKey' )
->method( 'selectRow' );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->never() )->method( 'set' );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
->method( 'selectRow' );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->never() )->method( 'set' );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeTitle:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
) );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
->will( $this->returnValue( false ) );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->never() )->method( 'set' );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
) );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
) );
$mockCache = $this->getMockCache();
- $mockDb->expects( $this->never() )
- ->method( 'get' );
- $mockDb->expects( $this->never() )
- ->method( 'set' );
- $mockDb->expects( $this->never() )
- ->method( 'delete' );
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
$store = $this->newWatchedItemStore(
$this->getMockLoadBalancer( $mockDb ),
'use strict';
const password = 'vagrant',
+ fs = require( 'fs' ),
path = require( 'path' ),
username = 'Admin';
chromeOptions: {
// Run headless when there is no DISPLAY
// --headless: since Chrome 59 https://chromium.googlesource.com/chromium/src/+/59.0.3030.0/headless/README.md
- args: process.env.DISPLAY ? [] : [ '--headless' ]
+ args: (
+ process.env.DISPLAY ? [] : [ '--headless' ]
+ ).concat(
+ // Disable Chrome sandbox when running in Docker
+ fs.existsSync( '/.dockerenv' ) ? [ '--no-sandbox' ] : []
+ )
}
} ],
//