'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php',
'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
+ 'Mediawiki\\Logger\\LogCapturingSpi' => __DIR__ . '/includes/debug/logger/LogCapturingSpi.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
}
private function fetch( $src, $integrity ) {
- $data = Http::get( $src, [ 'followRedirects' => false ] );
- if ( $data === false ) {
+ $req = MWHttpRequest::factory( $src, [ 'method' => 'GET', 'followRedirects' => false ] );
+ if ( !$req->execute()->isOK() ) {
throw new Exception( "Failed to download resource at {$src}" );
}
+ if ( $req->getStatus() !== 200 ) {
+ throw new Exception( "Unexpected HTTP {$req->getStatus()} response from {$src}" );
+ }
+ $data = $req->getContent();
$algo = $integrity === null ? $this->defaultAlgo : explode( '-', $integrity )[0];
$actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
if ( $integrity === $actualIntegrity ) {
$useReplica = ( $rigor !== 'secure' );
$block = $user->getBlock( $useReplica );
- // The block may explicitly allow an action (like "read" or "upload").
- if ( $block && $block->appliesToRight( $action ) === false ) {
+ // If the user does not have a block, or the block they do have explicitly
+ // allows the action (like "read" or "upload").
+ if ( !$block || $block->appliesToRight( $action ) === false ) {
return $errors;
}
if ( !$actionObj || $actionObj->requiresUnblock() ) {
if ( $user->isBlockedFrom( $this, $useReplica ) ) {
// @todo FIXME: Pass the relevant context into this function.
- $errors[] = $block
- ? $block->getPermissionsError( RequestContext::getMain() )
- : [ 'actionblockedtext' ];
+ $errors[] = $block->getPermissionsError( RequestContext::getMain() );
}
}
--- /dev/null
+<?php
+
+namespace Mediawiki\Logger;
+
+use Psr\Log\AbstractLogger;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Wraps another spi to capture all logs generated. This can be
+ * used, for example, to collect all logs generated during a
+ * unit test and report them when the test fails.
+ */
+class LogCapturingSpi implements Spi {
+ /** @var LoggerInterface[] */
+ private $singletons;
+ /** @var Spi */
+ private $inner;
+ /** @var array */
+ private $logs = [];
+
+ public function __construct( Spi $inner ) {
+ $this->inner = $inner;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLogs() {
+ return $this->logs;
+ }
+
+ /**
+ * @param string $channel
+ * @return LoggerInterface
+ */
+ public function getLogger( $channel ) {
+ if ( !isset( $this->singletons[$channel] ) ) {
+ $this->singletons[$channel] = $this->createLogger( $channel );
+ }
+ return $this->singletons[$channel];
+ }
+
+ /**
+ * @param array $log
+ */
+ public function capture( $log ) {
+ $this->logs[] = $log;
+ }
+
+ /**
+ * @param string $channel
+ * @return LoggerInterface
+ */
+ private function createLogger( $channel ) {
+ $inner = $this->inner->getLogger( $channel );
+ return new class( $channel, $inner, $this ) extends AbstractLogger {
+ /** @var string */
+ private $channel;
+ /** @var LoggerInterface */
+ private $logger;
+ /** @var LogCapturingSpi */
+ private $parent;
+
+ // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
+ public function __construct( $channel, LoggerInterface $logger, LogCapturingSpi $parent ) {
+ $this->channel = $channel;
+ $this->logger = $logger;
+ $this->parent = $parent;
+ }
+
+ // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
+ public function log( $level, $message, array $context = [] ) {
+ $this->parent->capture( [
+ 'channel' => $this->channel,
+ 'level' => $level,
+ 'message' => $message,
+ 'context' => $context
+ ] );
+ $this->logger->log( $level, $message, $context );
+ }
+ };
+ }
+}
}
/**
- * Check if a passwords meets the effective password policy for a User.
- * @param User $user who's policy we are checking
+ * Check if a password meets the effective password policy for a User.
+ * @param User $user whose policy we are checking
* @param string $password the password to check
* @return Status error to indicate the password didn't meet the policy, or fatal to
* indicate the user shouldn't be allowed to login. The status value will be an array,
}
/**
- * Check if a passwords meets the effective password policy for a User, using a set
+ * Check if a password meets the effective password policy for a User, using a set
* of groups they may or may not belong to. This function does not use the DB, so can
* be used in the installer.
- * @param User $user who's policy we are checking
+ * @param User $user whose policy we are checking
* @param string $password the password to check
* @param array $groups list of groups to which we assume the user belongs
* @return Status error to indicate the password didn't meet the policy, or fatal to
"blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"{{int:emailuser}}\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
"autoblockedtext": "Your IP address has been automatically blocked because it was used by another user, who was blocked by $1.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou may contact $1 or one of the other [[{{MediaWiki:Grouppage-sysop}}|administrators]] to discuss the block.\n\nNote that you may not use the \"{{int:emailuser}}\" feature unless you have a valid email address registered in your [[Special:Preferences|user preferences]] and you have not been blocked from using it.\n\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
"systemblockedtext": "Your username or IP address has been automatically blocked by MediaWiki.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYour current IP address is $3.\nPlease include all above details in any queries you make.",
- "actionblockedtext": "You have been blocked from performing this action.",
"blockednoreason": "no reason given",
"whitelistedittext": "Please $1 to edit pages.",
"confirmedittext": "You must confirm your email address before editing pages.\nPlease set and validate your email address through your [[Special:Preferences|user preferences]].",
"blockedtext": "Text displayed to blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link)\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Autoblockedtext|notext=1}}\n* {{msg-mw|Systemblockedtext|notext=1}}",
"autoblockedtext": "Text displayed to automatically blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block (in case of autoblocks: {{msg-mw|autoblocker}})\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link). Use it for GENDER.\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext|notext=1}}\n* {{msg-mw|Systemblockedtext|notext=1}}",
"systemblockedtext": "Text displayed to requests blocked by MediaWiki configuration.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - (Unused) A dummy user attributed as the blocker, possibly as a link to a user page.\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the dummy blocking user's username (plain text, without the link).\n* $5 - A short string indicating the type of system block.\n* $6 - the expiry of the block\n* $7 - the intended target of the block\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext|notext=1}}\n* {{msg-mw|Autoblockedtext|notext=1}}",
- "actionblockedtext": "Text displayed when a user is blocked from performing an action, but no matching block for the user exists. This can happen if an extension forces a user to be blocked.",
"blockednoreason": "Substituted with <code>$2</code> in the following message if the reason is not given:\n* {{msg-mw|cantcreateaccount-text}}.\n{{Identical|No reason given}}",
"whitelistedittext": "Used as error message. Parameters:\n* $1 - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description\n* $2 - an URL to the same\n\nSee also:\n* {{msg-mw|Nocreatetext}}\n* {{msg-mw|Uploadnologintext}}\n* {{msg-mw|Loginreqpagetext}}",
"confirmedittext": "Used as error message.",
'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
+ 'MediaWikiLoggerPHPUnitTestListener' => "$testDir/phpunit/MediaWikiLoggerPHPUnitTestListener.php",
'MediaWikiPHPUnitCommand' => "$testDir/phpunit/MediaWikiPHPUnitCommand.php",
+ 'MediaWikiPHPUnitResultPrinter' => "$testDir/phpunit/MediaWikiPHPUnitResultPrinter.php",
'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
'MediaWikiTestResult' => "$testDir/phpunit/MediaWikiTestResult.php",
--- /dev/null
+<?php
+
+use Mediawiki\Logger\LoggerFactory;
+use Mediawiki\Logger\Spi;
+use Mediawiki\Logger\LogCapturingSpi;
+
+/**
+ * Replaces the logging SPI on each test run. This allows
+ * another component (the printer) to fetch the logs when
+ * reporting why a test failed.
+ */
+class MediaWikiLoggerPHPUnitTestListener extends PHPUnit_Framework_BaseTestListener {
+ /** @var Spi|null */
+ private $originalSpi;
+ /** @var Spi|null */
+ private $spi;
+ /** @var array|null */
+ private $lastTestLogs;
+
+ /**
+ * A test started.
+ *
+ * @param PHPUnit_Framework_Test $test
+ */
+ public function startTest( PHPUnit_Framework_Test $test ) {
+ $this->lastTestLogs = null;
+ $this->originalSpi = LoggerFactory::getProvider();
+ $this->spi = new LogCapturingSpi( $this->originalSpi );
+ LoggerFactory::registerProvider( $this->spi );
+ }
+
+ /**
+ * A test ended.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param float $time
+ */
+ public function endTest( PHPUnit_Framework_Test $test, $time ) {
+ $this->lastTestLogs = $this->spi->getLogs();
+ LoggerFactory::registerProvider( $this->originalSpi );
+ $this->originalSpi = null;
+ $this->spi = null;
+ }
+
+ /**
+ * Get string formatted logs generated during the last
+ * test to execute.
+ *
+ * @return string
+ */
+ public function getLog() {
+ $logs = $this->lastTestLogs;
+ if ( !$logs ) {
+ return '';
+ }
+ $message = [];
+ foreach ( $logs as $log ) {
+ $message[] = sprintf(
+ '[%s] [%s] %s %s',
+ $log['channel'],
+ $log['level'],
+ $log['message'],
+ json_encode( $log['context'] )
+ );
+ }
+ return implode( "\n", $message );
+ }
+}
class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
private $cliArgs;
+ private $logListener;
public function __construct( $ignorableOptions, $cliArgs ) {
$ignore = function ( $arg ) {
$this->arguments['configuration'] = __DIR__ . '/suite.xml';
}
- // Add our own listener
+ // Add our own listeners
$this->arguments['listeners'][] = new MediaWikiPHPUnitTestListener;
+ $this->logListener = new MediaWikiLoggerPHPUnitTestListener;
+ $this->arguments['listeners'][] = $this->logListener;
// Output only to stderr to avoid "Headers already sent" problems
$this->arguments['stderr'] = true;
+
+ // We could create a printer instance and avoid passing the
+ // listener statically, but then we have to recreate the
+ // appropriate arguments handling + defaults.
+ if ( !isset( $this->arguments['printer'] ) ) {
+ $this->arguments['printer'] = MediaWikiPHPUnitResultPrinter::class;
+ }
}
protected function createRunner() {
+ MediaWikiPHPUnitResultPrinter::setLogListener( $this->logListener );
$runner = new MediaWikiTestRunner;
$runner->setMwCliArgs( $this->cliArgs );
return $runner;
--- /dev/null
+<?php
+
+class MediaWikiPHPUnitResultPrinter extends PHPUnit_TextUI_ResultPrinter {
+ /** @var MediaWikiLoggerPHPUnitTestListener */
+ private static $logListener;
+
+ public static function setLogListener( MediaWikiLoggerPHPUnitTestListener $logListener ) {
+ self::$logListener = $logListener;
+ }
+
+ protected function printDefectTrace( PHPUnit_Framework_TestFailure $defect ) {
+ $log = self::$logListener->getLog();
+ if ( $log ) {
+ $this->write( "=== Logs generated by test case\n{$log}\n===\n" );
+ }
+ parent::printDefectTrace( $defect );
+ }
+}
use MediaWiki\Logger\LegacySpi;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Logger\MonologSpi;
+use MediaWiki\Logger\LogCapturingSpi;
use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\IDatabase;
$this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
}
$singletons['loggers'][$channel] = $logger;
- } elseif ( $provider instanceof LegacySpi ) {
+ } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( !isset( $this->loggers[$channel] ) ) {
$this->loggers[$channel] = $singletons[$channel] ?? null;
}
} else {
$singletons['loggers'][$channel] = $logger;
}
- } elseif ( $provider instanceof LegacySpi ) {
+ } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( $logger === null ) {
unset( $singletons[$channel] );
} else {
$this->title->getUserPermissionsErrors( 'upload', $this->user ) );
$this->assertEquals( [],
$this->title->getUserPermissionsErrors( 'purge', $this->user ) );
+
+ // Test no block.
+ $this->user->mBlockedby = null;
+ $this->user->mBlock = null;
+
+ $this->assertEquals( [],
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
}
}
// Setting this enables automatic screenshots for when a browser command fails
// It is also used by afterTest for capturig failed assertions.
- screenshotPath: logPath,
+ // We disable it since we have our screenshot handler in the afterTest hook.
+ screenshotPath: null,
// Default timeout for each waitFor* command.
waitforTimeout: 10 * 1000,
*/
beforeTest: function ( test ) {
if ( process.env.DISPLAY && process.env.DISPLAY.startsWith( ':' ) ) {
- let videoPath = filePath( test, this.screenshotPath, 'mp4' );
+ let videoPath = filePath( test, logPath, 'mp4' );
const { spawn } = require( 'child_process' );
ffmpeg = spawn( 'ffmpeg', [
'-f', 'x11grab', // grab the X11 display
return;
}
// save screenshot
- let screenshotPath = filePath( test, this.screenshotPath, 'png' );
- browser.saveScreenshot( screenshotPath );
- console.log( '\n\tScreenshot location:', screenshotPath, '\n' );
+ let screenshotfile = filePath( test, logPath, 'png' );
+ browser.saveScreenshot( screenshotfile );
+ console.log( '\n\tScreenshot location:', screenshotfile, '\n' );
}
};