Merge "resourceloader: Change 'packageFiles' format to be JSON-compatible"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 22 Feb 2019 19:30:40 +0000 (19:30 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 22 Feb 2019 19:30:40 +0000 (19:30 +0000)
14 files changed:
autoload.php
includes/ForeignResourceManager.php
includes/Title.php
includes/debug/logger/LogCapturingSpi.php [new file with mode: 0644]
includes/password/UserPasswordPolicy.php
languages/i18n/en.json
languages/i18n/qqq.json
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php [new file with mode: 0644]
tests/phpunit/MediaWikiPHPUnitCommand.php
tests/phpunit/MediaWikiPHPUnitResultPrinter.php [new file with mode: 0644]
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/TitlePermissionTest.php
tests/selenium/wdio.conf.js

index a13763e..8eaecaa 100644 (file)
@@ -960,6 +960,7 @@ $wgAutoloadLocalClasses = [
        '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',
index d6175f6..18014fa 100644 (file)
@@ -132,10 +132,14 @@ class ForeignResourceManager {
        }
 
        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 ) {
index 6dd21c4..4075bd5 100644 (file)
@@ -2620,8 +2620,9 @@ class Title implements LinkTarget, IDBAccessObject {
                $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;
                }
 
@@ -2650,9 +2651,7 @@ class Title implements LinkTarget, IDBAccessObject {
                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() );
                        }
                }
 
diff --git a/includes/debug/logger/LogCapturingSpi.php b/includes/debug/logger/LogCapturingSpi.php
new file mode 100644 (file)
index 0000000..64d5563
--- /dev/null
@@ -0,0 +1,83 @@
+<?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 );
+                       }
+               };
+       }
+}
index 9eb921d..c61c795 100644 (file)
@@ -64,8 +64,8 @@ class UserPasswordPolicy {
        }
 
        /**
-        * 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,
@@ -83,10 +83,10 @@ class UserPasswordPolicy {
        }
 
        /**
-        * 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
index 7416d3f..c371cc2 100644 (file)
        "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]].",
index 796d436..6860220 100644 (file)
        "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.",
index 0245572..f742a1b 100644 (file)
@@ -55,7 +55,9 @@ $wgAutoloadClasses += [
        '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",
diff --git a/tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php b/tests/phpunit/MediaWikiLoggerPHPUnitTestListener.php
new file mode 100644 (file)
index 0000000..502685d
--- /dev/null
@@ -0,0 +1,68 @@
+<?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 );
+       }
+}
index 8979195..5d139ff 100644 (file)
@@ -2,6 +2,7 @@
 
 class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
        private $cliArgs;
+       private $logListener;
 
        public function __construct( $ignorableOptions, $cliArgs ) {
                $ignore = function ( $arg ) {
@@ -18,14 +19,24 @@ class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
                        $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;
diff --git a/tests/phpunit/MediaWikiPHPUnitResultPrinter.php b/tests/phpunit/MediaWikiPHPUnitResultPrinter.php
new file mode 100644 (file)
index 0000000..e796752
--- /dev/null
@@ -0,0 +1,18 @@
+<?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 );
+       }
+}
index 287d28c..35f396e 100644 (file)
@@ -3,6 +3,7 @@
 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;
@@ -1124,7 +1125,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                                $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;
                        }
@@ -1151,7 +1152,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                                } else {
                                        $singletons['loggers'][$channel] = $logger;
                                }
-                       } elseif ( $provider instanceof LegacySpi ) {
+                       } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
                                if ( $logger === null ) {
                                        unset( $singletons[$channel] );
                                } else {
index cb5e1f8..1157331 100644 (file)
@@ -1029,5 +1029,12 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        $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 ) );
        }
 }
index 916ee74..11be135 100644 (file)
@@ -117,7 +117,8 @@ exports.config = {
 
        // 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,
@@ -153,7 +154,7 @@ exports.config = {
        */
        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
@@ -196,8 +197,8 @@ exports.config = {
                        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' );
        }
 };