* $wgEnableSpecialMute (T218265) - This configuration controls whether
Special:Mute is available and whether to include a link to it on emails
originating from Special:Email.
+* editmyuserjsredirect user right – users without this right now cannot edit JS
+ redirects in their userspace unless the target of the redirect is also in
+ their userspace. By default, this right is given to everyone.
==== Changed configuration ====
* $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four
of headers in private wikis.
* Language::formatTimePeriod now supports the new 'avoidhours' option to output
strings like "5 days ago" instead of "5 days 13 hours ago".
+* (T220163) Added SpecialMuteModifyFormFields hook to allow extensions
+ to add fields to Special:Mute.
=== External library changes in 1.34 ===
&$oldTitle: old title (object)
&$newTitle: new title (object)
+'SpecialMuteModifyFormFields': Add more fields to Special:Mute
+$sp: SpecialPage object, for context
+&$fields: Current HTMLForm fields descriptors
+
'SpecialNewpagesConditions': Called when building sql query for
Special:NewPages.
&$special: NewPagesPager object (subclass of ReverseChronologicalPager)
$wgGroupPermissions['user']['editmyusercss'] = true;
$wgGroupPermissions['user']['editmyuserjson'] = true;
$wgGroupPermissions['user']['editmyuserjs'] = true;
+$wgGroupPermissions['user']['editmyuserjsredirect'] = true;
$wgGroupPermissions['user']['purge'] = true;
$wgGroupPermissions['user']['sendemail'] = true;
$wgGroupPermissions['user']['applychangetags'] = true;
* @return string
*/
function wfGlobalCacheKey( ...$args ) {
+ wfDeprecated( __METHOD__, '1.30' );
return ObjectCache::getLocalClusterInstance()->makeGlobalKey( ...$args );
}
use Exception;
use Hooks;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Session\SessionManager;
use MediaWiki\Special\SpecialPageFactory;
use MediaWiki\User\UserIdentity;
/** @var SpecialPageFactory */
private $specialPageFactory;
+ /** @var RevisionLookup */
+ private $revisionLookup;
+
/** @var string[] List of pages names anonymous user may see */
private $whitelistRead;
'editmyusercss',
'editmyuserjson',
'editmyuserjs',
+ 'editmyuserjsredirect',
'editmywatchlist',
'editsemiprotected',
'editsitecss',
/**
* @param SpecialPageFactory $specialPageFactory
+ * @param RevisionLookup $revisionLookup
* @param string[] $whitelistRead
* @param string[] $whitelistReadRegexp
* @param bool $emailConfirmToEdit
*/
public function __construct(
SpecialPageFactory $specialPageFactory,
+ RevisionLookup $revisionLookup,
$whitelistRead,
$whitelistReadRegexp,
$emailConfirmToEdit,
NamespaceInfo $nsInfo
) {
$this->specialPageFactory = $specialPageFactory;
+ $this->revisionLookup = $revisionLookup;
$this->whitelistRead = $whitelistRead;
$this->whitelistReadRegexp = $whitelistReadRegexp;
$this->emailConfirmToEdit = $emailConfirmToEdit;
&& !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
) {
$errors[] = [ 'mycustomjsprotected', $action ];
+ } elseif (
+ $page->isUserJsConfigPage()
+ && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
+ ) {
+ // T207750 - do not allow users to edit a redirect if they couldn't edit the target
+ $rev = $this->revisionLookup->getRevisionByTitle( $page );
+ $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
+ $target = $content ? $content->getUltimateRedirectTarget() : null;
+ if ( $target && (
+ !$target->inNamespace( NS_USER )
+ || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
+ ) ) {
+ $errors[] = [ 'mycustomjsredirectprotected', $action ];
+ }
}
} else {
// Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
$config = $services->getMainConfig();
return new PermissionManager(
$services->getSpecialPageFactory(),
+ $services->getRevisionLookup(),
$config->get( 'WhitelistRead' ),
$config->get( 'WhitelistReadRegexp' ),
$config->get( 'EmailConfirmToEdit' ),
}
/**
- * @param bool|IResultWrapper $row
+ * @param bool|stdClass $row
* @return void
*/
protected function setCurrent( $row ) {
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\DBTransactionError;
/**
* Class for managing the deferred updates
* @since 1.34
*/
public static function attemptUpdate( DeferrableUpdate $update, ILBFactory $lbFactory ) {
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ if ( !$ticket || $lbFactory->hasTransactionRound() ) {
+ throw new DBTransactionError( null, "A database transaction round is pending." );
+ }
+
if ( $update instanceof DataUpdate ) {
- $update->setTransactionTicket( $lbFactory->getEmptyTransactionTicket( __METHOD__ ) );
+ $update->setTransactionTicket( $ticket );
}
- if (
+ $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ $useExplicitTrxRound = !(
$update instanceof TransactionRoundAwareUpdate &&
$update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
- ) {
- $fnameTrxOwner = null;
+ );
+ // Flush any pending changes left over from an implicit transaction round
+ if ( $useExplicitTrxRound ) {
+ $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round
} else {
- $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ $lbFactory->commitMasterChanges( $fnameTrxOwner ); // new implicit round
}
-
- if ( $fnameTrxOwner !== null ) {
- $lbFactory->beginMasterChanges( $fnameTrxOwner );
- }
-
+ // Run the update after any stale master view snapshots have been flushed
$update->doUpdate();
-
- if ( $fnameTrxOwner !== null ) {
- $lbFactory->commitMasterChanges( $fnameTrxOwner );
- }
+ // Commit any pending changes from the explicit or implicit transaction round
+ $lbFactory->commitMasterChanges( $fnameTrxOwner );
}
/**
/**
* @deprecated DO NOT CALL ME.
- * This method was introduced when factoring UploadImporter out of WikiRevision.
- * It only has 1 use by the deprecated downloadSource method in WikiRevision.
- * Do not use this in new code.
+ * This method was introduced when factoring (Importable)UploadRevisionImporter out of
+ * WikiRevision. It only has 1 use by the deprecated downloadSource method in WikiRevision.
+ * Do not use this in new code, it will be made private soon.
*
* @param ImportableUploadRevision $wikiRevision
*
/**
* @since 1.12.2
- * @deprecated in 1.31. Use UploadImporter::import
+ * @deprecated in 1.31. Use UploadRevisionImporter::import
* @return bool
*/
public function importUpload() {
/**
* @since 1.12.2
- * @deprecated in 1.31. Use UploadImporter::downloadSource
+ * @deprecated in 1.31. No replacement
* @return bool|string
*/
public function downloadSource() {
/** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */
protected $attrMap = [];
- /** Bitfield constants for get()/getMulti() */
- const READ_LATEST = 1; // use latest data for replicated stores
- const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
- /** Bitfield constants for set()/merge() */
- const WRITE_SYNC = 4; // synchronously write to all locations for replicated stores
- const WRITE_CACHE_ONLY = 8; // Only change state of the in-memory cache
- const WRITE_ALLOW_SEGMENTS = 16; // Allow partitioning of the value if it is large
- const WRITE_PRUNE_SEGMENTS = 32; // Delete all partition segments of the value
+ /** Bitfield constants for get()/getMulti(); these are only advisory */
+ const READ_LATEST = 1; // if supported, avoid reading stale data due to replication
+ const READ_VERIFIED = 2; // promise that the caller handles detection of staleness
+ /** Bitfield constants for set()/merge(); these are only advisory */
+ const WRITE_SYNC = 4; // if supported, block until the write is fully replicated
+ const WRITE_CACHE_ONLY = 8; // only change state of the in-memory cache
+ const WRITE_ALLOW_SEGMENTS = 16; // allow partitioning of the value if it is large
+ const WRITE_PRUNE_SEGMENTS = 32; // delete all the segments if the value is partitioned
+ const WRITE_BACKGROUND = 64; // if supported,
/** @var string Component to use for key construction of blob segment keys */
const SEGMENT_COMPONENT = 'segment';
*
* This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
*
+ * WRITE_BACKGROUND can be used for bulk insertion where the response is not vital
+ *
* @param mixed[] $data Map of (key => value)
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
*
* This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
*
+ * WRITE_BACKGROUND can be used for bulk deletion where the response is not vital
+ *
* @param string[] $keys List of keys
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
/** @var Memcached */
- protected $client;
+ protected $syncClient;
+ /** @var Memcached|null */
+ protected $asyncClient;
+
+ /** @var bool Whether the non-buffering client is locked from use */
+ protected $syncClientIsBuffering = false;
+ /** @var bool Whether the non-buffering client should be flushed before use */
+ protected $hasUnflushedChanges = false;
+
+ /** @var array Memcached options */
+ private static $OPTS_SYNC_WRITES = [
+ Memcached::OPT_NO_BLOCK => false, // async I/O (using TCP buffers)
+ Memcached::OPT_BUFFER_WRITES => false // libmemcached buffers
+ ];
+ /** @var array Memcached options */
+ private static $OPTS_ASYNC_WRITES = [
+ Memcached::OPT_NO_BLOCK => true, // async I/O (using TCP buffers)
+ Memcached::OPT_BUFFER_WRITES => true // libmemcached buffers
+ ];
/**
* Available parameters are:
// The Memcached object is essentially shared for each pool ID.
// We can only reuse a pool ID if we keep the config consistent.
$connectionPoolId = md5( serialize( $params ) );
- $client = new Memcached( $connectionPoolId );
- $this->initializeClient( $client, $params );
+ $syncClient = new Memcached( "$connectionPoolId-sync" );
+ // Avoid clobbering the main thread-shared Memcached instance
+ $asyncClient = new Memcached( "$connectionPoolId-async" );
} else {
- $client = new Memcached;
- $this->initializeClient( $client, $params );
+ $syncClient = new Memcached();
+ $asyncClient = null;
}
- $this->client = $client;
+ $this->initializeClient( $syncClient, $params, self::$OPTS_SYNC_WRITES );
+ if ( $asyncClient ) {
+ $this->initializeClient( $asyncClient, $params, self::$OPTS_ASYNC_WRITES );
+ }
+ // Set the main client and any dedicated one for buffered writes
+ $this->syncClient = $syncClient;
+ $this->asyncClient = $asyncClient;
// The compression threshold is an undocumented php.ini option for some
// reason. There's probably not much harm in setting it globally, for
// compatibility with the settings for the PHP client.
*
* @param Memcached $client
* @param array $params
+ * @param array $options Base options for Memcached::setOptions()
* @throws RuntimeException
*/
- private function initializeClient( Memcached $client, array $params ) {
+ private function initializeClient( Memcached $client, array $params, array $options ) {
if ( $client->getServerList() ) {
$this->logger->debug( __METHOD__ . ": pre-initialized client instance." );
$this->logger->debug( __METHOD__ . ": initializing new client instance." );
- $options = [
+ $options += [
+ Memcached::OPT_NO_BLOCK => false,
+ Memcached::OPT_BUFFER_WRITES => false,
// Network protocol (ASCII or binary)
Memcached::OPT_BINARY_PROTOCOL => $params['use_binary_protocol'],
// Set various network timeouts
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$this->debug( "get($key)" );
+
+ $client = $this->acquireSyncClient();
if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
/** @noinspection PhpUndefinedClassConstantInspection */
$flags = Memcached::GET_EXTENDED;
- $res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags );
+ $res = $client->get( $this->validateKeyEncoding( $key ), null, $flags );
if ( is_array( $res ) ) {
$result = $res['value'];
$casToken = $res['cas'];
$casToken = null;
}
} else {
- $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
+ $result = $client->get( $this->validateKeyEncoding( $key ), null, $casToken );
}
- $result = $this->checkResult( $key, $result );
- return $result;
+
+ return $this->checkResult( $key, $result );
}
protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "set($key)" );
- $result = $this->client->set(
+
+ $client = $this->acquireSyncClient();
+ $result = $client->set(
$this->validateKeyEncoding( $key ),
$value,
$this->fixExpiry( $exptime )
);
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) {
+
+ return ( $result === false && $client->getResultCode() === Memcached::RES_NOTSTORED )
// "Not stored" is always used as the mcrouter response with AllAsyncRoute
- return true;
- }
- return $this->checkResult( $key, $result );
+ ? true
+ : $this->checkResult( $key, $result );
}
protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "cas($key)" );
- $result = $this->client->cas( $casToken, $this->validateKeyEncoding( $key ),
- $value, $this->fixExpiry( $exptime ) );
+
+ $result = $this->acquireSyncClient()->cas(
+ $casToken,
+ $this->validateKeyEncoding( $key ),
+ $value, $this->fixExpiry( $exptime )
+ );
+
return $this->checkResult( $key, $result );
}
protected function doDelete( $key, $flags = 0 ) {
$this->debug( "delete($key)" );
- $result = $this->client->delete( $this->validateKeyEncoding( $key ) );
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
+
+ $client = $this->acquireSyncClient();
+ $result = $client->delete( $this->validateKeyEncoding( $key ) );
+
+ return ( $result === false && $client->getResultCode() === Memcached::RES_NOTFOUND )
// "Not found" is counted as success in our interface
- return true;
- }
- return $this->checkResult( $key, $result );
+ ? true
+ : $this->checkResult( $key, $result );
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "add($key)" );
- $result = $this->client->add(
+
+ $result = $this->acquireSyncClient()->add(
$this->validateKeyEncoding( $key ),
$value,
$this->fixExpiry( $exptime )
);
+
return $this->checkResult( $key, $result );
}
public function incr( $key, $value = 1 ) {
$this->debug( "incr($key)" );
- $result = $this->client->increment( $key, $value );
+
+ $result = $this->acquireSyncClient()->increment( $key, $value );
+
return $this->checkResult( $key, $result );
}
public function decr( $key, $value = 1 ) {
$this->debug( "decr($key)" );
- $result = $this->client->decrement( $key, $value );
+
+ $result = $this->acquireSyncClient()->decrement( $key, $value );
+
return $this->checkResult( $key, $result );
}
if ( $result !== false ) {
return $result;
}
- switch ( $this->client->getResultCode() ) {
+
+ $client = $this->syncClient;
+ switch ( $client->getResultCode() ) {
case Memcached::RES_SUCCESS:
break;
case Memcached::RES_DATA_EXISTS:
case Memcached::RES_NOTSTORED:
case Memcached::RES_NOTFOUND:
- $this->debug( "result: " . $this->client->getResultMessage() );
+ $this->debug( "result: " . $client->getResultMessage() );
break;
default:
- $msg = $this->client->getResultMessage();
+ $msg = $client->getResultMessage();
$logCtx = [];
if ( $key !== false ) {
- $server = $this->client->getServerByKey( $key );
+ $server = $client->getServerByKey( $key );
$logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
$logCtx['memcached-key'] = $key;
- $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
+ $msg = "Memcached error for key \"{memcached-key}\" " .
+ "on server \"{memcached-server}\": $msg";
} else {
$msg = "Memcached error: $msg";
}
protected function doGetMulti( array $keys, $flags = 0 ) {
$this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
+
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->getMulti( $keys ) ?: [];
+
+ // The PECL implementation uses "gets" which works as well as a pipeline
+ $result = $this->acquireSyncClient()->getMulti( $keys ) ?: [];
+
return $this->checkResult( false, $result );
}
protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
+
+ $exptime = $this->fixExpiry( $exptime );
foreach ( array_keys( $data ) as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
+
+ // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
+ // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852
+ if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+ $client = $this->acquireAsyncClient();
+ $result = $client->setMulti( $data, $exptime );
+ $this->releaseAsyncClient( $client );
+ } else {
+ $result = $this->acquireSyncClient()->setMulti( $data, $exptime );
+ }
+
return $this->checkResult( false, $result );
}
protected function doDeleteMulti( array $keys, $flags = 0 ) {
$this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
+
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->deleteMulti( $keys ) ?: [];
- $ok = true;
- foreach ( $result as $code ) {
+
+ // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
+ // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852
+ if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+ $client = $this->acquireAsyncClient();
+ $resultArray = $client->deleteMulti( $keys ) ?: [];
+ $this->releaseAsyncClient( $client );
+ } else {
+ $resultArray = $this->acquireSyncClient()->deleteMulti( $keys ) ?: [];
+ }
+
+ $result = true;
+ foreach ( $resultArray as $code ) {
if ( !in_array( $code, [ true, Memcached::RES_NOTFOUND ], true ) ) {
// "Not found" is counted as success in our interface
- $ok = false;
+ $result = false;
}
}
- return $this->checkResult( false, $ok );
+
+ return $this->checkResult( false, $result );
}
protected function doChangeTTL( $key, $exptime, $flags ) {
$this->debug( "touch($key)" );
- $result = $this->client->touch( $key, $exptime );
+
+ $result = $this->acquireSyncClient()->touch( $key, $this->fixExpiry( $exptime ) );
+
return $this->checkResult( $key, $result );
}
return $value;
}
- $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+ $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER );
if ( $serializer === Memcached::SERIALIZER_PHP ) {
return serialize( $value );
} elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
return (int)$value;
}
- $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+ $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER );
if ( $serializer === Memcached::SERIALIZER_PHP ) {
return unserialize( $value );
} elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
throw new UnexpectedValueException( __METHOD__ . ": got serializer '$serializer'." );
}
+
+ /**
+ * @return Memcached
+ */
+ private function acquireSyncClient() {
+ if ( $this->syncClientIsBuffering ) {
+ throw new RuntimeException( "The main (unbuffered I/O) client is locked" );
+ }
+
+ if ( $this->hasUnflushedChanges ) {
+ // Force a synchronous flush of async writes so that their changes are visible
+ $this->syncClient->fetch();
+ if ( $this->asyncClient ) {
+ $this->asyncClient->fetch();
+ }
+ $this->hasUnflushedChanges = false;
+ }
+
+ return $this->syncClient;
+ }
+
+ /**
+ * @return Memcached
+ */
+ private function acquireAsyncClient() {
+ if ( $this->asyncClient ) {
+ return $this->asyncClient; // dedicated buffering instance
+ }
+
+ // Modify the main instance to temporarily buffer writes
+ $this->syncClientIsBuffering = true;
+ $this->syncClient->setOptions( self::$OPTS_ASYNC_WRITES );
+
+ return $this->syncClient;
+ }
+
+ /**
+ * @param Memcached $client
+ */
+ private function releaseAsyncClient( $client ) {
+ $this->hasUnflushedChanges = true;
+
+ if ( !$this->asyncClient ) {
+ // This is the main instance; make it stop buffering writes again
+ $client->setOptions( self::$OPTS_SYNC_WRITES );
+ $this->syncClientIsBuffering = false;
+ }
+ }
}
}
try {
- $conn->watch( $key );
- if ( $conn->exists( $key ) ) {
- $conn->multi( Redis::MULTI );
- $conn->incrBy( $key, $value );
- $batchResult = $conn->exec();
- if ( $batchResult === false ) {
- $result = false;
- } else {
- $result = end( $batchResult );
- }
- } else {
- $result = false;
- $conn->unwatch();
- }
- } catch ( RedisException $e ) {
- try {
- $conn->unwatch(); // sanity
- } catch ( RedisException $ex ) {
- // already errored
- }
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'incr', $key, $conn->getServer(), $result );
-
- return $result;
- }
-
- public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
- $conn = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
-
- $ttl = $this->convertToRelative( $exptime );
- $preIncrInit = $init - $value;
- try {
- $conn->multi( Redis::MULTI );
- $conn->set( $key, $preIncrInit, $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ] );
- $conn->incrBy( $key, $value );
- $batchResult = $conn->exec();
- if ( $batchResult === false ) {
- $result = false;
- $this->debug( "incrWithInit request to {$conn->getServer()} failed" );
- } else {
- $result = end( $batchResult );
+ if ( !$conn->exists( $key ) ) {
+ return false;
}
+ // @FIXME: on races, the key may have a 0 TTL
+ $result = $conn->incrBy( $key, $value );
} catch ( RedisException $e ) {
$result = false;
$this->handleException( $conn, $e );
throw new MWException( __METHOD__ . ": invalid versionCallback for file" .
" \"{$fileInfo['name']}\" in module \"{$this->getName()}\"" );
}
- $expanded['definitionSummary'] = ( $fileInfo['versionCallback'] )( $context );
+ $expanded['definitionSummary'] =
+ ( $fileInfo['versionCallback'] )( $context, $this->getConfig() );
// Don't invoke 'callback' here as it may be expensive (T223260).
$expanded['callback'] = $fileInfo['callback'];
} else {
- $expanded['content'] = ( $fileInfo['callback'] )( $context );
+ $expanded['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
}
} elseif ( isset( $fileInfo['config'] ) ) {
if ( $type !== 'data' ) {
$fileInfo['content'] = $content;
unset( $fileInfo['filePath'] );
} elseif ( isset( $fileInfo['callback'] ) ) {
- $fileInfo['content'] = ( $fileInfo['callback'] )( $context );
+ $fileInfo['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
unset( $fileInfo['callback'] );
}
// accidentally returning it so best check and fix
$status = Status::wrap( $status );
} elseif ( is_string( $status ) ) {
- $status = Status::newFatal( new RawMessage( '$1', $status ) );
+ $status = Status::newFatal( new RawMessage( '$1', [ $status ] ) );
} elseif ( is_array( $status ) ) {
if ( is_string( reset( $status ) ) ) {
$status = Status::newFatal( ...$status );
*/
class SpecialMute extends FormSpecialPage {
+ const PAGE_NAME = 'Mute';
+
/** @var User */
private $target;
$this->centralIdLookup = CentralIdLookup::factory();
- parent::__construct( 'Mute', '', false );
+ parent::__construct( self::PAGE_NAME, '', false );
}
/**
parent::execute( $par );
$out = $this->getOutput();
- $out->addModules( 'mediawiki.special.pageLanguage' );
+ $out->addModules( 'mediawiki.misc-authed-ooui' );
}
/**
* @return bool
*/
public function onSubmit( array $data, HTMLForm $form = null ) {
- if ( !empty( $data['MuteEmail'] ) ) {
- $this->muteEmailsFromTarget();
- } else {
- $this->unmuteEmailsFromTarget();
+ foreach ( $data as $userOption => $value ) {
+ if ( $value ) {
+ $this->muteTarget( $userOption );
+ } else {
+ $this->unmuteTarget( $userOption );
+ }
}
return true;
}
/**
- * Un-mute emails from target
+ * Un-mute target
+ *
+ * @param string $userOption up_property key that holds the blacklist
*/
- private function unmuteEmailsFromTarget() {
- $blacklist = $this->getBlacklist();
+ private function unmuteTarget( $userOption ) {
+ $blacklist = $this->getBlacklist( $userOption );
$key = array_search( $this->targetCentralId, $blacklist );
if ( $key !== false ) {
$blacklist = implode( "\n", $blacklist );
$user = $this->getUser();
- $user->setOption( 'email-blacklist', $blacklist );
+ $user->setOption( $userOption, $blacklist );
$user->saveSettings();
}
}
/**
- * Mute emails from target
+ * Mute target
+ * @param string $userOption up_property key that holds the blacklist
*/
- private function muteEmailsFromTarget() {
+ private function muteTarget( $userOption ) {
// avoid duplicates just in case
- if ( !$this->isTargetBlacklisted() ) {
- $blacklist = $this->getBlacklist();
+ if ( !$this->isTargetBlacklisted( $userOption ) ) {
+ $blacklist = $this->getBlacklist( $userOption );
$blacklist[] = $this->targetCentralId;
$blacklist = implode( "\n", $blacklist );
$user = $this->getUser();
- $user->setOption( 'email-blacklist', $blacklist );
+ $user->setOption( $userOption, $blacklist );
$user->saveSettings();
}
}
/**
* @inheritDoc
*/
- protected function alterForm( HTMLForm $form ) {
+ protected function getForm() {
+ $form = parent::getForm();
$form->setId( 'mw-specialmute-form' );
$form->setHeaderText( $this->msg( 'specialmute-header', $this->target )->parse() );
$form->setSubmitTextMsg( 'specialmute-submit' );
$form->setSubmitID( 'save' );
+
+ return $form;
}
/**
* @inheritDoc
*/
protected function getFormFields() {
- if ( !$this->enableUserEmailBlacklist || !$this->enableUserEmail ) {
- throw new ErrorPageError( 'specialmute', 'specialmute-error-email-blacklist-disabled' );
+ $fields = [];
+ if (
+ $this->enableUserEmailBlacklist &&
+ $this->enableUserEmail &&
+ $this->getUser()->getEmailAuthenticationTimestamp()
+ ) {
+ $fields['email-blacklist'] = [
+ 'type' => 'check',
+ 'label-message' => 'specialmute-label-mute-email',
+ 'default' => $this->isTargetBlacklisted( 'email-blacklist' ),
+ ];
}
- if ( !$this->getUser()->getEmailAuthenticationTimestamp() ) {
- throw new ErrorPageError( 'specialmute', 'specialmute-error-email-preferences' );
- }
+ Hooks::run( 'SpecialMuteModifyFormFields', [ $this, &$fields ] );
- $fields['MuteEmail'] = [
- 'type' => 'check',
- 'label-message' => 'specialmute-label-mute-email',
- 'default' => $this->isTargetBlacklisted(),
- ];
+ if ( count( $fields ) == 0 ) {
+ throw new ErrorPageError( 'specialmute', 'specialmute-error-no-options' );
+ }
return $fields;
}
}
/**
+ * @param string $userOption
* @return bool
*/
- private function isTargetBlacklisted() {
- $blacklist = $this->getBlacklist();
- return in_array( $this->targetCentralId, $blacklist );
+ public function isTargetBlacklisted( $userOption ) {
+ $blacklist = $this->getBlacklist( $userOption );
+ return in_array( $this->targetCentralId, $blacklist, true );
}
/**
+ * @param string $userOption
* @return array
*/
- private function getBlacklist() {
- $blacklist = $this->getUser()->getOption( 'email-blacklist' );
+ private function getBlacklist( $userOption ) {
+ $blacklist = $this->getUser()->getOption( $userOption );
if ( !$blacklist ) {
return [];
}
"right-editmyusercss": "Edit your own user CSS files",
"right-editmyuserjson": "Edit your own user JSON files",
"right-editmyuserjs": "Edit your own user JavaScript files",
+ "right-editmyuserjsredirect": "Edit your own user JavaScript files that are redirects",
"right-viewmywatchlist": "View your own watchlist",
"right-editmywatchlist": "Edit your own watchlist. Note some actions will still add pages even without this right.",
"right-viewmyprivateinfo": "View your own private data (e.g. email address, real name)",
"action-editmyusercss": "edit your own user CSS files",
"action-editmyuserjson": "edit your own user JSON files",
"action-editmyuserjs": "edit your own user JavaScript files",
+ "action-editmyuserjsredirect": "edit your own user JavaScript files that are redirects",
"action-viewsuppressed": "view revisions hidden from any user",
"action-hideuser": "block a username, hiding it from the public",
"action-ipblock-exempt": "bypass IP blocks, auto-blocks and range blocks",
"specialmute-success": "Your mute preferences have been updated. See all muted users in [[Special:Preferences|your preferences]].",
"specialmute-submit": "Confirm",
"specialmute-label-mute-email": "Mute emails from this user",
- "specialmute-header": "Please select your mute preferences for {{BIDI:[[User:$1]]}}.",
+ "specialmute-header": "Please select your mute preferences for <b>{{BIDI:[[User:$1]]}}</b>.",
"specialmute-error-invalid-user": "The username requested could not be found.",
- "specialmute-error-email-blacklist-disabled": "Muting users from sending you emails is not enabled.",
- "specialmute-error-email-preferences": "You must confirm your email address before you can mute a user. You may do so from [[Special:Preferences]].",
+ "specialmute-error-no-options": "Mute features are unavailable. This might be because: you haven't confirmed your email address or the wiki administrator has disabled email features and/or email blacklist for this wiki.",
"specialmute-email-footer": "To manage email preferences for {{BIDI:$2}} please visit <$1>.",
"specialmute-login-required": "Please log in to change your mute preferences.",
"mute-preferences": "Mute preferences",
"passwordpolicies-policy-passwordnotinlargeblacklist": "Password cannot be in the list of 100,000 most commonly used passwords.",
"passwordpolicies-policyflag-forcechange": "must change on login",
"passwordpolicies-policyflag-suggestchangeonlogin": "suggest change on login",
+ "mycustomjsredirectprotected": "You do not have permission to edit this JavaScript page because it is a redirect and it does not point inside your userspace.",
"easydeflate-invaliddeflate": "Content provided is not properly deflated",
"unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
"userlogout-continue": "Do you want to log out?"
"right-editsitejs": "{{doc-right|editsitejs}}",
"right-editmyusercss": "{{doc-right|editmyusercss}}\nSee also:\n* {{msg-mw|Right-editusercss}}",
"right-editmyuserjson": "{{doc-right|editmyuserjson}}\nSee also:\n* {{msg-mw|Right-edituserjson}}",
- "right-editmyuserjs": "{{doc-right|editmyuserjs}}\nSee also:\n* {{msg-mw|Right-edituserjs}}",
+ "right-editmyuserjs": "{{doc-right|editmyuserjs}}\nSee also:\n* {{msg-mw|Right-edituserjs}}\n* {{msg-mw|Right-editmyuserjsredirect}}",
+ "right-editmyuserjsredirect": "{{doc-right|editmyuserjsredirect}}\nSame as {{msg-mw|Right-editmyuserjs}} except if page is a redirect.\n\nSee also:\n* {{msg-mw|Right-edituserjs}}",
"right-viewmywatchlist": "{{doc-right|viewmywatchlist}}",
"right-editmywatchlist": "{{doc-right|editmywatchlist}}",
"right-viewmyprivateinfo": "{{doc-right|viewmyprivateinfo}}",
"action-editmyusercss": "{{doc-action|editmyusercss}}",
"action-editmyuserjson": "{{doc-action|editmyuserjson}}",
"action-editmyuserjs": "{{doc-action|editmyuserjs}}",
+ "action-editmyuserjsredirect": "{{doc-action|editmyuserjsredirect}}",
"action-viewsuppressed": "{{doc-action|viewsuppressed}}",
"action-hideuser": "{{doc-action|hideuser}}",
"action-ipblock-exempt": "{{doc-action|ipblock-exempt}}",
"specialmute-label-mute-email": "Label for the checkbox that mutes/unmutes emails from the specified user.",
"specialmute-header": "Used as header text on [[Special:Mute]]. Shown before the form with the muting options.\n* $1 - User selected for muting",
"specialmute-error-invalid-user": "Error displayed when the username cannot be found.",
- "specialmute-error-email-blacklist-disabled": "Error displayed when email blacklist is not enabled.",
- "specialmute-error-email-preferences": "Error displayed when the user has not confirmed their email address.",
+ "specialmute-error-no-options": "Error displayed when there are no options available to mute on [[Special:Mute]].",
"specialmute-email-footer": "Email footer in plain text linking to [[Special:Mute]] preselecting the sender to manage muting options.\n* $1 - Url linking to [[Special:Mute]].\n* $2 - The user sending the email.",
"specialmute-login-required": "Error displayed when a user tries to access [[Special:Mute]] before logging in.",
"mute-preferences": "Link in the sidebar to manage muting preferences for a user. It links to [[Special:Mute]] with the user in context as the subpage.",
"passwordpolicies-policy-passwordnotinlargeblacklist": "Password policy that enforces that a password is not in a list of 100,000 number of \"popular\" passwords.",
"passwordpolicies-policyflag-forcechange": "Password policy flag that enforces changing invalid passwords on login.",
"passwordpolicies-policyflag-suggestchangeonlogin": "Password policy flag that suggests changing invalid passwords on login.",
+ "mycustomjsredirectprotected": "Error message shown when user tries to edit their own JS page that is a foreign redirect without the 'mycustomjsredirectprotected' right. See also {{mw-msg|mycustomjsprotected}}.",
"easydeflate-invaliddeflate": "Error message if the content passed to easydeflate was not deflated (compressed) properly",
"unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
"userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
. " memcached server and shows a report" );
$this->addOption( 'i', 'Number of iterations', false, true );
$this->addOption( 'cache', 'Use servers from this $wgObjectCaches store', false, true );
+ $this->addOption( 'driver', 'Either "php" or "pecl"', false, true );
$this->addArg( 'server[:port]', 'Memcached server to test, with optional port', false );
}
# find out the longest server string to nicely align output later on
$maxSrvLen = $servers ? max( array_map( 'strlen', $servers ) ) : 0;
+ $type = $this->getOption( 'driver', 'php' );
+ if ( $type === 'php' ) {
+ $class = MemcachedPhpBagOStuff::class;
+ } elseif ( $type === 'pecl' ) {
+ $class = MemcachedPeclBagOStuff::class;
+ } else {
+ $this->fatalError( "Invalid driver type '$type'" );
+ }
+
foreach ( $servers as $server ) {
- $this->output(
- str_pad( $server, $maxSrvLen ),
- $server # output channel
- );
+ $this->output( str_pad( $server, $maxSrvLen ) . "\n" );
- $mcc = new MemcachedClient( [
- 'persistant' => true,
+ /** @var BagOStuff $mcc */
+ $mcc = new $class( [
+ 'servers' => [ $server ],
+ 'persistent' => true,
'timeout' => $wgMemCachedTimeout
] );
- $mcc->set_servers( [ $server ] );
- $set = 0;
- $incr = 0;
- $get = 0;
- $time_start = microtime( true );
- for ( $i = 1; $i <= $iterations; $i++ ) {
- if ( $mcc->set( "test$i", $i ) ) {
- $set++;
- }
+
+ $this->benchmarkSingleKeyOps( $mcc, $iterations );
+ $this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations );
+ $this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations );
+ }
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkSingleKeyOps( $mcc, $iterations ) {
+ $add = 0;
+ $set = 0;
+ $incr = 0;
+ $get = 0;
+ $delete = 0;
+
+ $keys = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keys[] = "test$i";
+ }
+
+ // Clear out any old values
+ $mcc->deleteMulti( $keys );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->add( $key, $i ) ) {
+ $add++;
}
- for ( $i = 1; $i <= $iterations; $i++ ) {
- if ( !is_null( $mcc->incr( "test$i", $i ) ) ) {
- $incr++;
- }
+ }
+ $addMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->set( $key, $i ) ) {
+ $set++;
+ }
+ }
+ $setMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( !is_null( $mcc->incr( $key, $i ) ) ) {
+ $incr++;
}
- for ( $i = 1; $i <= $iterations; $i++ ) {
- $value = $mcc->get( "test$i" );
- if ( $value == $i * 2 ) {
- $get++;
- }
+ }
+ $incrMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ $value = $mcc->get( $key );
+ if ( $value == $i * 2 ) {
+ $get++;
}
- $exectime = microtime( true ) - $time_start;
+ }
+ $getMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
- $this->output( " set: $set incr: $incr get: $get time: $exectime", $server );
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->delete( $key ) ) {
+ $delete++;
+ }
}
+ $delMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " add: $add/$iterations {$addMs}ms " .
+ "set: $set/$iterations {$setMs}ms " .
+ "incr: $incr/$iterations {$incrMs}ms " .
+ "get: $get/$iterations ({$getMs}ms) " .
+ "delete: $delete/$iterations ({$delMs}ms)\n"
+ );
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ) {
+ $keysByValue = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keysByValue["test$i"] = 'S' . str_pad( $i, 2048 );
+ }
+ $keyList = array_keys( $keysByValue );
+
+ $time_start = microtime( true );
+ $mSetOk = $mcc->setMulti( $keysByValue ) ? 'S' : 'F';
+ $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $found = $mcc->getMulti( $keyList );
+ $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+ $mGetOk = 0;
+ foreach ( $found as $key => $value ) {
+ $mGetOk += ( $value === $keysByValue[$key] );
+ }
+
+ $time_start = microtime( true );
+ $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600 ) ? 'S' : 'F';
+ $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $mDelOk = $mcc->deleteMulti( $keyList ) ? 'S' : 'F';
+ $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " setMulti (IB): $mSetOk {$mSetMs}ms " .
+ "getMulti (IB): $mGetOk/$iterations {$mGetMs}ms " .
+ "changeTTLMulti (IB): $mChangeTTLOk {$mChangeTTTMs}ms " .
+ "deleteMulti (IB): $mDelOk {$mDelMs}ms\n"
+ );
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ) {
+ $flags = $mcc::WRITE_BACKGROUND;
+ $keysByValue = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keysByValue["test$i"] = 'A' . str_pad( $i, 2048 );
+ }
+ $keyList = array_keys( $keysByValue );
+
+ $time_start = microtime( true );
+ $mSetOk = $mcc->setMulti( $keysByValue, 0, $flags ) ? 'S' : 'F';
+ $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $found = $mcc->getMulti( $keyList );
+ $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+ $mGetOk = 0;
+ foreach ( $found as $key => $value ) {
+ $mGetOk += ( $value === $keysByValue[$key] );
+ }
+
+ $time_start = microtime( true );
+ $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600, $flags ) ? 'S' : 'F';
+ $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $mDelOk = $mcc->deleteMulti( $keyList, $flags ) ? 'S' : 'F';
+ $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " setMulti (DB): $mSetOk {$mSetMs}ms " .
+ "getMulti (DB): $mGetOk/$iterations {$mGetMs}ms " .
+ "changeTTLMulti (DB): $mChangeTTLOk {$mChangeTTTMs}ms " .
+ "deleteMulti (DB): $mDelOk {$mDelMs}ms\n"
+ );
}
}
'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.jqueryMsg",
'packageFiles' => [
'mediawiki.jqueryMsg.js',
- [ 'name' => 'parserDefaults.json', 'callback' => function ( ResourceLoaderContext $context ) {
+ [ 'name' => 'parserDefaults.json', 'callback' => function (
+ ResourceLoaderContext $context, Config $config
+ ) {
$tagData = Sanitizer::getRecognizedTagData();
$allowedHtmlElements = array_merge(
array_keys( $tagData['htmlpairs'] ),
);
$magicWords = [
- 'SITENAME' => $context->getConfig()->get( 'Sitename' ),
+ 'SITENAME' => $config->get( 'Sitename' ),
];
Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] );
*/
public function testWfGlobalCacheKey() {
$cache = ObjectCache::getLocalClusterInstance();
+ $this->hideDeprecated( 'wfGlobalCacheKey' );
$this->assertEquals(
$cache->makeGlobalKey( 'foo', 123, 'bar' ),
wfGlobalCacheKey( 'foo', 123, 'bar' )
namespace MediaWiki\Tests\Permissions;
use Action;
+use ContentHandler;
use FauxRequest;
-use MediaWiki\Session\SessionId;
-use MediaWiki\Session\TestUtils;
-use MediaWikiLangTestCase;
-use RequestContext;
-use stdClass;
-use Title;
-use User;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\SystemBlock;
+use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionLookup;
use Wikimedia\ScopedCallback;
+use MediaWiki\Session\SessionId;
+use MediaWiki\Session\TestUtils;
+use MediaWikiLangTestCase;
+use RequestContext;
+use stdClass;
+use Title;
+use User;
use Wikimedia\TestingAccessWrapper;
/**
);
}
+ public function testJsConfigRedirectEditPermissions() {
+ $revision = null;
+ $user = $this->getTestUser()->getUser();
+ $otherUser = $this->getTestUser( 'sysop' )->getUser();
+ $localJsTitle = Title::newFromText( 'User:' . $user->getName() . '/foo.js' );
+ $otherLocalJsTitle = Title::newFromText( 'User:' . $user->getName() . '/foo2.js' );
+ $nonlocalJsTitle = Title::newFromText( 'User:' . $otherUser->getName() . '/foo.js' );
+
+ $services = MediaWikiServices::getInstance();
+ $revisionLookup = $this->getMockBuilder( RevisionLookup::class )
+ ->setMethods( [ 'getRevisionByTitle' ] )
+ ->getMockForAbstractClass();
+ $revisionLookup->method( 'getRevisionByTitle' )
+ ->willReturnCallback( function ( LinkTarget $page ) use (
+ $services, &$revision, $localJsTitle
+ ) {
+ if ( $localJsTitle->equals( Title::newFromLinkTarget( $page ) ) ) {
+ return $revision;
+ } else {
+ return $services->getRevisionLookup()->getRevisionByTitle( $page );
+ }
+ } );
+ $permissionManager = new PermissionManager(
+ $services->getSpecialPageFactory(),
+ $revisionLookup,
+ [],
+ [],
+ false,
+ false,
+ [],
+ [],
+ [],
+ MediaWikiServices::getInstance()->getNamespaceInfo()
+ );
+ $this->setService( 'PermissionManager', $permissionManager );
+
+ $permissionManager->overrideUserRightsForTesting( $user, [ 'edit', 'editmyuserjs' ] );
+
+ $revision = $this->getJavascriptRevision( $localJsTitle, $user, '/* script */' );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $otherLocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $nonlocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [ [ 'mycustomjsredirectprotected', 'edit' ] ], $errors );
+
+ $permissionManager->overrideUserRightsForTesting( $user,
+ [ 'edit', 'editmyuserjs', 'editmyuserjsredirect' ] );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $nonlocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+ }
+
/**
* @todo This test method should be split up into separate test methods and
* data providers
$this->assertFalse( $permissionManager->userHasRight( $this->user, 'move' ) );
}
+ /**
+ * Create a RevisionRecord with a single Javascript main slot.
+ * @param Title $title
+ * @param User $user
+ * @param string $text
+ * @return MutableRevisionRecord
+ */
+ private function getJavascriptRevision( Title $title, User $user, $text ) {
+ $content = ContentHandler::makeContent( $text, $title, CONTENT_MODEL_JAVASCRIPT );
+ $revision = new MutableRevisionRecord( $title );
+ $revision->setContent( 'main', $content );
+ return $revision;
+ }
+
+ /**
+ * Create a RevisionRecord with a single Javascript redirect main slot.
+ * @param Title $title
+ * @param Title $redirectTargetTitle
+ * @param User $user
+ * @return MutableRevisionRecord
+ */
+ private function getJavascriptRedirectRevision(
+ Title $title, Title $redirectTargetTitle, User $user
+ ) {
+ $content = ContentHandler::getForModelID( CONTENT_MODEL_JAVASCRIPT )
+ ->makeRedirectContent( $redirectTargetTitle );
+ $revision = new MutableRevisionRecord( $title );
+ $revision->setContent( 'main', $content );
+ return $revision;
+ }
+
}
/**
* @covers SpecialMute::execute
- * @expectedExceptionMessage Muting users from sending you emails is not enabled
+ * @expectedExceptionMessage Mute features are unavailable
* @expectedException ErrorPageError
*/
public function testEmailBlacklistNotEnabled() {
+ $this->setTemporaryHook(
+ 'SpecialMuteModifyFormFields',
+ null
+ );
+
$this->setMwGlobals( [
'wgEnableUserEmailBlacklist' => false
] );
$loggedInUser->confirmEmail();
$loggedInUser->saveSettings();
- $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => 1 ], true );
+ $fauxRequest = new FauxRequest( [ 'wpemail-blacklist' => true ], true );
list( $html, ) = $this->executeSpecialPage(
$targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser
);
$loggedInUser->confirmEmail();
$loggedInUser->saveSettings();
- $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => false ], true );
+ $fauxRequest = new FauxRequest( [ 'wpemail-blacklist' => false ], true );
list( $html, ) = $this->executeSpecialPage(
$targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser
);
$this->assertEquals(
'wiki',
$this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
- 'get(): simple setting on an non-existing wiki'
+ 'get(): simple setting on a non-existing wiki'
);
// Fallback
$this->assertEquals(
'wiki',
$this->mConf->get( 'Fallback', 'eswiki', 'wiki' ),
- 'get(): fallback setting on an non-existing wiki'
+ 'get(): fallback setting on a non-existing wiki'
);
$this->assertEquals(
'tag',
$this->mConf->get( 'Fallback', 'eswiki', 'wiki', [], [ 'tag' ] ),
- 'get(): fallback setting on an non-existing wiki (with wiki tag)'
+ 'get(): fallback setting on a non-existing wiki (with wiki tag)'
);
// Merging
$this->assertEquals(
$common,
$this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
- 'get(): merging setting on an non-existing wiki'
+ 'get(): merging setting on a non-existing wiki'
);
$this->assertEquals(
$commonTag,
$this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
- 'get(): merging setting on an non-existing wiki (with tag)'
+ 'get(): merging setting on a non-existing wiki (with tag)'
);
}
$this->assertEquals(
'es wiki eswiki',
$this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
- 'get(): parameter replacement on an non-existing wiki'
+ 'get(): parameter replacement on a non-existing wiki'
);
}