From: jenkins-bot Date: Wed, 28 Aug 2019 17:34:58 +0000 (+0000) Subject: Merge "Remove unused localisation message 'wlshowlast'" X-Git-Tag: 1.34.0-rc.0~520 X-Git-Url: http://git.cyclocoop.org/fichier?a=commitdiff_plain;h=08986c7c0932bc88714660d4ed103c1e417c3a3e;hp=5e90299f384dba2e4973550e4f0003f69ab40790;p=lhc%2Fweb%2Fwiklou.git Merge "Remove unused localisation message 'wlshowlast'" --- diff --git a/Gruntfile.js b/Gruntfile.js index f3950f6f50..8115ea2aec 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,7 +26,7 @@ module.exports = function ( grunt ) { cache: true }, all: [ - '**/*.js{,on}', + '**/*.{js,json}', '!docs/**', '!node_modules/**', '!resources/lib/**', diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 932122d8ae..f7790cb628 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -476,6 +476,7 @@ because of Phabricator reports. SearchResult::newFromTitle(). This class is being refactored into an abstract class. If you extend this class please be sure to override all its methods or extend RevisionSearchResult. +* Skin::getSkinNameMessages() is deprecated and no longer used. === Other changes in 1.34 === * … diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 28b0a4b714..fabe4a2e7c 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -71,18 +71,15 @@ class ApiFeedContributions extends ApiBase { ' [' . $config->get( 'LanguageCode' ) . ']'; $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); - $target = 'newbies'; - if ( $params['user'] != 'newbies' ) { - try { - $target = $this->titleParser - ->parseTitle( $params['user'], NS_USER ) - ->getText(); - } catch ( MalformedTitleException $e ) { - $this->dieWithError( - [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ], - 'baduser_' . $this->encodeParamName( 'user' ) - ); - } + try { + $target = $this->titleParser + ->parseTitle( $params['user'], NS_USER ) + ->getText(); + } catch ( MalformedTitleException $e ) { + $this->dieWithError( + [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ], + 'baduser_' . $this->encodeParamName( 'user' ) + ); } $feed = new $feedClasses[$params['feedformat']] ( diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 8b31f05c83..9e04d09632 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -150,6 +150,7 @@ class FileBackendGroup { $class = $config['class']; if ( $class === FileBackendMultiWrite::class ) { + // @todo How can we test this? What's the intended use-case? foreach ( $config['backends'] as $index => $beConfig ) { if ( isset( $beConfig['template'] ) ) { // Config is just a modified version of a registered backend's. diff --git a/includes/libs/filebackend/filejournal/FileJournal.php b/includes/libs/filebackend/filejournal/FileJournal.php index dc007a0cec..e51242375a 100644 --- a/includes/libs/filebackend/filejournal/FileJournal.php +++ b/includes/libs/filebackend/filejournal/FileJournal.php @@ -26,6 +26,7 @@ * @ingroup FileJournal */ +use Wikimedia\ObjectFactory; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -43,12 +44,12 @@ abstract class FileJournal { protected $ttlDays; /** - * Construct a new instance from configuration. + * Construct a new instance from configuration. Do not call this directly, use factory(). * * @param array $config Includes: * 'ttlDays' : days to keep log entries around (false means "forever") */ - protected function __construct( array $config ) { + public function __construct( array $config ) { $this->ttlDays = $config['ttlDays'] ?? false; } @@ -61,11 +62,10 @@ abstract class FileJournal { * @return FileJournal */ final public static function factory( array $config, $backend ) { - $class = $config['class']; - $jrn = new $class( $config ); - if ( !$jrn instanceof self ) { - throw new InvalidArgumentException( "$class is not an instance of " . __CLASS__ ); - } + $jrn = ObjectFactory::getObjectFromSpec( + $config, + [ 'specIsArg' => true, 'assertClass' => __CLASS__ ] + ); $jrn->backend = $backend; return $jrn; diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index eeed05e95f..212ac47157 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -61,9 +61,11 @@ abstract class Skin extends ContextSource { /** * Fetch the skinname messages for available skins. + * @deprecated since 1.34, no longer used. * @return string[] */ static function getSkinNameMessages() { + wfDeprecated( __METHOD__, '1.34' ); $messages = []; foreach ( self::getSkinNames() as $skinKey => $skinName ) { $messages[] = "skinname-$skinKey"; @@ -1311,19 +1313,21 @@ abstract class Skin extends ContextSource { * @return array */ public function buildSidebar() { + $services = MediaWikiServices::getInstance(); $callback = function ( $old = null, &$ttl = null ) { $bar = []; $this->addToSidebar( $bar, 'sidebar' ); Hooks::run( 'SkinBuildSidebar', [ $this, &$bar ] ); - if ( MessageCache::singleton()->isDisabled() ) { + $msgCache = MediaWikiServices::getInstance()->getMessageCache(); + if ( $msgCache->isDisabled() ) { $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069 } return $bar; }; - $msgCache = MessageCache::singleton(); - $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $msgCache = $services->getMessageCache(); + $wanCache = $services->getMainWANObjectCache(); $config = $this->getConfig(); $sidebar = $config->get( 'EnableSidebarCache' ) diff --git a/languages/i18n/en.json b/languages/i18n/en.json index fbcd1f8e77..106d6a7dc6 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -3402,8 +3402,6 @@ "img-lang-default": "(default language)", "img-lang-info": "Render this image in $1. $2", "img-lang-go": "Go", - "ascending_abbrev": "asc", - "descending_abbrev": "desc", "table_pager_next": "Next page", "table_pager_prev": "Previous page", "table_pager_first": "First page", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 2d5ceb89d7..5fdcb7dc37 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -3611,8 +3611,6 @@ "img-lang-default": "An option in the drop down of a translatable file. For example see [[:File:Gerrit patchset 25838 test.svg]].\n\nUsed when it cannot be determined what the default fallback language is.\n\nHowever it should be noted that most of the time, the content displayed for this option would be in English.\n{{Identical|Default language}}", "img-lang-info": "Label for drop down box. Appears underneath the image on the image description page. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nParameters:\n* $1 - a drop down box with language options, uses the following messages:\n** {{msg-mw|Img-lang-default}}\n** {{msg-mw|Img-lang-opt}}. e.g. \"English (en)\", \"日本語 (ja)\"\n* $2 - a submit button, which uses the text from {{msg-mw|Img-lang-go}}", "img-lang-go": "Go button for the language select for translatable files. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nSee also:\n* {{msg-mw|img-lang-info}}\n{{Identical|Go}}", - "ascending_abbrev": "Abbreviation of ascending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}", - "descending_abbrev": "Abbreviation of descending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}", "table_pager_next": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Next page}}", "table_pager_prev": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Previous page}}", "table_pager_first": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|First page}}", diff --git a/resources/Resources.php b/resources/Resources.php index d33e3dee2c..72fe627e1c 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1042,6 +1042,12 @@ return [ 'mediawiki.pager.tablePager' => [ 'styles' => 'resources/src/mediawiki.pager.tablePager/TablePager.less', ], + 'mediawiki.pulsatingdot' => [ + 'styles' => [ + 'resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less', + ], + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.searchSuggest' => [ 'targets' => [ 'desktop', 'mobile' ], 'scripts' => 'resources/src/mediawiki.searchSuggest/searchSuggest.js', diff --git a/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less b/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less new file mode 100644 index 0000000000..00a5608230 --- /dev/null +++ b/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less @@ -0,0 +1,70 @@ +.mw-pulsating-dot { + &:before, + &:after { + content: ''; + display: block; + position: absolute; + border-radius: 50%; + background-color: #36c; + } + + &:before { + width: 36px; + height: 36px; + top: -18px; + left: -18px; + opacity: 0; + -webkit-animation: mw-pulsating-dot-pulse 3s ease-out; + -moz-animation: mw-pulsating-dot-pulse 3s ease-out; + animation: mw-pulsating-dot-pulse 3s ease-out; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + animation-iteration-count: infinite; + } + + &:after { + width: 12px; + height: 12px; + top: -6px; + left: -6px; + } +} + +.mw-pulsating-dot-pulse-frames() { + 0% { + transform: scale( 0 ); + opacity: 0; + } + + 25% { + transform: scale( 0 ); + opacity: 0.1; + } + + 50% { + transform: scale( 0.1 ); + opacity: 0.3; + } + + 75% { + transform: scale( 0.5 ); + opacity: 0.5; + } + + 100% { + transform: scale( 1 ); + opacity: 0; + } +} + +@-webkit-keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} + +@-moz-keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} + +@keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js index 5ca39d5d45..dfc41be8eb 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js @@ -110,7 +110,9 @@ // We have no way to display a translated placeholder for custom formats placeholderDateFormat = ''; } else { - // Messages: mw-widgets-dateinput-placeholder-day, mw-widgets-dateinput-placeholder-month + // The following messages are used here: + // * mw-widgets-dateinput-placeholder-day + // * mw-widgets-dateinput-placeholder-month placeholderDateFormat = mw.msg( 'mw-widgets-dateinput-placeholder-' + config.precision ); } diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index 7c8df1a6f5..9f1d67be16 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -221,6 +221,9 @@ $wgAutoloadClasses += [ # tests/phpunit/unit/includes 'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php", + # tests/phpunit/unit/includes/filebackend + 'FileBackendGroupTestTrait' => "$testDir/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php", + # tests/phpunit/unit/includes/libs/filebackend/fsfile 'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php", diff --git a/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php b/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php new file mode 100644 index 0000000000..ee3262c4e8 --- /dev/null +++ b/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php @@ -0,0 +1,57 @@ +getLockManagerGroupFactory(); + } + + private function newObj( array $options = [] ) : FileBackendGroup { + $globals = [ 'DirectoryMode', 'FileBackends', 'ForeignFileRepos', 'LocalFileRepo' ]; + foreach ( $globals as $global ) { + $this->setMwGlobals( + "wg$global", $options[$global] ?? self::getDefaultOptions()[$global] ); + } + + $serviceMembers = [ + 'configuredROMode' => 'ConfiguredReadOnlyMode', + 'srvCache' => 'LocalServerObjectCache', + 'wanCache' => 'MainWANObjectCache', + 'mimeAnalyzer' => 'MimeAnalyzer', + 'lmgFactory' => 'LockManagerGroupFactory', + 'tmpFileFactory' => 'TempFSFileFactory', + ]; + + foreach ( $serviceMembers as $key => $name ) { + if ( isset( $options[$key] ) ) { + $this->setService( $name, $options[$key] ); + } + } + + $this->assertEmpty( + array_diff( array_keys( $options ), $globals, array_keys( $serviceMembers ) ) ); + + $this->resetServices(); + FileBackendGroup::destroySingleton(); + + $services = MediaWikiServices::getInstance(); + + foreach ( $serviceMembers as $key => $name ) { + $this->$key = $services->getService( $name ); + } + + return FileBackendGroup::singleton(); + } +} diff --git a/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php b/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php new file mode 100644 index 0000000000..d23f645eeb --- /dev/null +++ b/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php @@ -0,0 +1,459 @@ + LocalRepo::class, + 'name' => 'local', + 'directory' => 'upload-dir', + 'thumbDir' => 'thumb/', + 'transcodedDir' => 'transcoded/', + 'fileMode' => 0664, + 'scriptDirUrl' => 'script-path/', + 'url' => 'upload-path/', + 'hashLevels' => 2, + 'thumbScriptUrl' => false, + 'transformVia404' => false, + 'deletedDir' => 'deleted/', + 'deletedHashLevels' => 3, + 'backend' => 'local-backend', + ]; + } + + private static function getDefaultOptions() { + return [ + 'DirectoryMode' => 0775, + 'FileBackends' => [], + 'ForeignFileRepos' => [], + 'LocalFileRepo' => self::getDefaultLocalFileRepo(), + 'wikiId' => self::getWikiID(), + ]; + } + + /** + * @covers ::__construct + */ + public function testConstructor_overrideImplicitBackend() { + $obj = $this->newObj( [ 'FileBackends' => + [ [ 'name' => 'local-backend', 'class' => '', 'lockManager' => 'fsLockManager' ] ] + ] ); + $this->assertSame( '', $obj->config( 'local-backend' )['class'] ); + } + + /** + * @covers ::__construct + */ + public function testConstructor_backendObject() { + // 'backend' being an object makes that repo from configuration ignored + // XXX This is not documented in DefaultSettings.php, does it do anything useful? + $obj = $this->newObj( [ 'ForeignFileRepos' => [ [ 'backend' => new stdclass ] ] ] ); + $this->assertSame( FSFileBackend::class, $obj->config( 'local-backend' )['class'] ); + } + + /** + * @dataProvider provideRegister_domainId + * @param string $key Key to check in return value of config() + * @param string|callable $expected Expected value of config()[$key], or callable returning it + * @param array $extraBackendsOptions To add to the FileBackends entry passed to newObj() + * @param array $otherExtraOptions To add to the array passed to newObj() (e.g., services) + * @covers ::register + */ + public function testRegister( + $key, $expected, array $extraBackendsOptions = [], array $otherExtraOptions = [] + ) { + if ( $expected instanceof Closure ) { + // Lame hack to get around providers being called too early + $expected = $expected(); + } + if ( $key === 'domainId' ) { + // This will change the expected LMG name too + $otherExtraOptions['lmgFactory'] = $this->getLockManagerGroupFactory( $expected ); + } + $obj = $this->newObj( $otherExtraOptions + [ + 'FileBackends' => [ + $extraBackendsOptions + [ + 'name' => 'myname', 'class' => '', 'lockManager' => 'fsLockManager' + ] + ], + ] ); + $this->assertSame( $expected, $obj->config( 'myname' )[$key] ); + } + + public static function provideRegister_domainId() { + return [ + 'domainId with neither wikiId nor domainId set' => [ + 'domainId', + function () { + return self::getWikiID(); + }, + ], + 'domainId with wikiId set but no domainId' => + [ 'domainId', 'id0', [ 'wikiId' => 'id0' ] ], + 'domainId with wikiId and domainId set' => + [ 'domainId', 'dom1', [ 'wikiId' => 'id0', 'domainId' => 'dom1' ] ], + 'readOnly without readOnly set' => [ 'readOnly', false ], + 'readOnly with readOnly set to string' => + [ 'readOnly', 'cuz', [ 'readOnly' => 'cuz' ] ], + 'readOnly without readOnly set but with string in passed object' => [ + 'readOnly', + 'cuz', + [], + [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], + ], + 'readOnly with readOnly set to false but string in passed object' => [ + 'readOnly', + false, + [ 'readOnly' => false ], + [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], + ], + ]; + } + + /** + * @dataProvider provideRegister_exception + * @param array $fileBackends Value of FileBackends to pass to constructor + * @param string $class Expected exception class + * @param string $msg Expected exception message + * @covers ::__construct + * @covers ::register + */ + public function testRegister_exception( $fileBackends, $class, $msg ) { + $this->setExpectedException( $class, $msg ); + $this->newObj( [ 'FileBackends' => $fileBackends ] ); + } + + public static function provideRegister_exception() { + return [ + 'Nameless' => [ + [ [] ], InvalidArgumentException::class, "Cannot register a backend with no name." + ], + 'Duplicate' => [ + [ [ 'name' => 'dupe', 'class' => '' ], [ 'name' => 'dupe' ] ], + LogicException::class, + "Backend with name 'dupe' already registered.", + ], + 'Classless' => [ + [ [ 'name' => 'classless' ] ], + InvalidArgumentException::class, + "Backend with name 'classless' has no class.", + ], + ]; + } + + /** + * @covers ::__construct + * @covers ::config + * @covers ::get + */ + public function testGet() { + $backend = $this->newObj()->get( 'local-backend' ); + $this->assertTrue( $backend instanceof FSFileBackend ); + } + + /** + * @covers ::get + */ + public function testGetUnrecognized() { + $this->setExpectedException( InvalidArgumentException::class, + "No backend defined with the name 'unrecognized'." ); + $this->newObj()->get( 'unrecognized' ); + } + + /** + * @covers ::__construct + * @covers ::config + */ + public function testConfig() { + $obj = $this->newObj(); + $config = $obj->config( 'local-backend' ); + + // XXX How to actually test that a profiler is loaded? + $this->assertNull( $config['profiler']( 'x' ) ); + // Equality comparison doesn't work for closures, so just set to null + $config['profiler'] = null; + + $this->assertEquals( [ + 'mimeCallback' => [ $obj, 'guessMimeInternal' ], + 'obResetFunc' => 'wfResetOutputBuffers', + 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], + 'tmpFileFactory' => $this->tmpFileFactory, + 'statusWrapper' => [ Status::class, 'wrap' ], + 'wanCache' => $this->wanCache, + 'srvCache' => $this->srvCache, + 'logger' => LoggerFactory::getInstance( 'FileOperation' ), + // This was set to null above in $config, it's not really null + 'profiler' => null, + 'name' => 'local-backend', + 'containerPaths' => [ + 'local-public' => 'upload-dir', + 'local-thumb' => 'thumb/', + 'local-transcoded' => 'transcoded/', + 'local-deleted' => 'deleted/', + 'local-temp' => 'upload-dir/temp', + ], + 'fileMode' => 0664, + 'directoryMode' => 0775, + 'domainId' => self::getWikiID(), + 'readOnly' => false, + 'class' => FSFileBackend::class, + 'lockManager' => + $this->lmgFactory->getLockManagerGroup( self::getWikiID() )->get( 'fsLockManager' ), + 'fileJournal' => + FileJournal::factory( [ 'class' => NullFileJournal::class ], 'local-backend' ), + ], $config ); + + // For config values that are objects, check object identity. + $this->assertSame( [ $obj, 'guessMimeInternal' ], $config['mimeCallback'] ); + $this->assertSame( $this->tmpFileFactory, $config['tmpFileFactory'] ); + $this->assertSame( $this->wanCache, $config['wanCache'] ); + $this->assertSame( $this->srvCache, $config['srvCache'] ); + } + + /** + * @dataProvider provideConfig_default + * @param string $expected Expected default value + * @param string $inputName Name to set to null in LocalFileRepo setting + * @param string|array $key Key to check in array returned by config(), or array [ 'key1', + * 'key2' ] for nested key + * @covers ::__construct + * @covers ::config + */ + public function testConfig_defaultNull( $expected, $inputName, $key ) { + $config = self::getDefaultLocalFileRepo(); + $config[$inputName] = null; + + $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); + + $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; + + $this->assertSame( $expected, $actual ); + } + + /** + * @dataProvider provideConfig_default + * @param string $expected Expected default value + * @param string $inputName Name to unset in LocalFileRepo setting + * @param string|array $key Key to check in array returned by config(), or array [ 'key1', + * 'key2' ] for nested key + * @covers ::__construct + * @covers ::config + */ + public function testConfig_defaultUnset( $expected, $inputName, $key ) { + $config = self::getDefaultLocalFileRepo(); + unset( $config[$inputName] ); + + $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); + + $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; + + $this->assertSame( $expected, $actual ); + } + + public static function provideConfig_default() { + return [ + 'deletedDir' => [ false, 'deletedDir', [ 'containerPaths', 'local-deleted' ] ], + 'thumbDir' => [ 'upload-dir/thumb', 'thumbDir', [ 'containerPaths', 'local-thumb' ] ], + 'transcodedDir' => [ + 'upload-dir/transcoded', 'transcodedDir', [ 'containerPaths', 'local-transcoded' ] + ], + 'fileMode' => [ 0644, 'fileMode', 'fileMode' ], + ]; + } + + /** + * @covers ::config + */ + public function testConfig_fileJournal() { + $mockJournal = $this->createMock( FileJournal::class ); + $mockJournal->expects( $this->never() )->method( $this->anything() ); + + $obj = $this->newObj( [ 'FileBackends' => [ [ + 'name' => 'name', + 'class' => '', + 'lockManager' => 'fsLockManager', + 'fileJournal' => [ 'factory' => + function () use ( $mockJournal ) { + return $mockJournal; + } + ], + ] ] ] ); + + $this->assertSame( $mockJournal, $obj->config( 'name' )['fileJournal'] ); + } + + /** + * @covers ::config + */ + public function testConfigUnrecognized() { + $this->setExpectedException( InvalidArgumentException::class, + "No backend defined with the name 'unrecognized'." ); + $this->newObj()->config( 'unrecognized' ); + } + + /** + * @dataProvider provideBackendFromPath + * @covers ::backendFromPath + * @param string|null $expected Name of backend that will be returned from 'get', or null + * @param string $storagePath + */ + public function testBackendFromPath( $expected = null, $storagePath ) { + $obj = $this->newObj( [ 'FileBackends' => [ + [ 'name' => '', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + [ 'name' => 'a', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + [ 'name' => 'b', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + ] ] ); + $this->assertSame( + $expected === null ? null : $obj->get( $expected ), + $obj->backendFromPath( $storagePath ) + ); + } + + public static function provideBackendFromPath() { + return [ + 'Empty string' => [ null, '' ], + 'mwstore://' => [ null, 'mwstore://' ], + 'mwstore://a' => [ null, 'mwstore://a' ], + 'mwstore:///' => [ null, 'mwstore:///' ], + 'mwstore://a/' => [ null, 'mwstore://a/' ], + 'mwstore://a//' => [ null, 'mwstore://a//' ], + 'mwstore://a/b' => [ 'a', 'mwstore://a/b' ], + 'mwstore://a/b/' => [ 'a', 'mwstore://a/b/' ], + 'mwstore://a/b////' => [ 'a', 'mwstore://a/b////' ], + 'mwstore://a/b/c' => [ 'a', 'mwstore://a/b/c' ], + 'mwstore://a/b/c/d' => [ 'a', 'mwstore://a/b/c/d' ], + 'mwstore://b/b' => [ 'b', 'mwstore://b/b' ], + 'mwstore://c/b' => [ null, 'mwstore://c/b' ], + ]; + } + + /** + * @dataProvider provideGuessMimeInternal + * @covers ::guessMimeInternal + * @param string $storagePath + * @param string|null $content + * @param string|null $fsPath + * @param string|null $expectedExtensionType Expected return of + * MimeAnalyzer::guessTypesForExtension + * @param string|null $expectedGuessedMimeType Expected return value of + * MimeAnalyzer::guessMimeType (null if expected not to be called) + */ + public function testGuessMimeInternal( + $storagePath, + $content, + $fsPath, + $expectedExtensionType, + $expectedGuessedMimeType + ) { + $mimeAnalyzer = $this->createMock( MimeAnalyzer::class ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessTypesForExtension' ) + ->willReturn( $expectedExtensionType ); + $tmpFileFactory = $this->createMock( TempFSFileFactory::class ); + + if ( !$expectedExtensionType && $fsPath ) { + $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) + ->with( $fsPath, false )->willReturn( $expectedGuessedMimeType ); + } elseif ( !$expectedExtensionType && strlen( $content ) ) { + // XXX What should we do about the file creation here? Really we should mock + // file_put_contents() somehow. It's not very nice to ignore the value of + // $wgTmpDirectory. + $tmpFile = ( new TempFSFileFactory() )->newTempFSFile( 'mime_', '' ); + + $tmpFileFactory->expects( $this->once() )->method( 'newTempFSFile' ) + ->with( 'mime_', '' )->willReturn( $tmpFile ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) + ->with( $tmpFile->getPath(), false )->willReturn( $expectedGuessedMimeType ); + } else { + $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); + $mimeAnalyzer->expects( $this->never() )->method( 'guessMimeType' ); + } + + $mimeAnalyzer->expects( $this->never() ) + ->method( $this->anythingBut( 'guessTypesForExtension', 'guessMimeType' ) ); + $tmpFileFactory->expects( $this->never() ) + ->method( $this->anythingBut( 'newTempFSFile' ) ); + + $obj = $this->newObj( [ + 'mimeAnalyzer' => $mimeAnalyzer, + 'tmpFileFactory' => $tmpFileFactory, + ] ); + + $this->assertSame( $expectedExtensionType ?? $expectedGuessedMimeType ?? 'unknown/unknown', + $obj->guessMimeInternal( $storagePath, $content, $fsPath ) ); + } + + public static function provideGuessMimeInternal() { + return [ + 'With extension' => + [ 'foo.txt', null, null, 'text/plain', null ], + 'No extension' => + [ 'foo', null, null, null, null ], + 'Empty content, with extension' => + [ 'foo.txt', '', null, 'text/plain', null ], + 'Empty content, no extension' => + [ 'foo', '', null, null, null ], + 'Non-empty content, with extension' => + [ 'foo.txt', 'foo', null, 'text/plain', null ], + 'Non-empty content, no extension' => + [ 'foo', 'foo', null, null, 'text/html' ], + 'Empty path, with extension' => + [ 'foo.txt', null, '', 'text/plain', null ], + 'Empty path, no extension' => + [ 'foo', null, '', null, null ], + 'Non-empty path, with extension' => + [ 'foo.txt', null, '/bogus/path', 'text/plain', null ], + 'Non-empty path, no extension' => + [ 'foo', null, '/bogus/path', null, 'text/html' ], + 'Empty path and content, with extension' => + [ 'foo.txt', '', '', 'text/plain', null ], + 'Empty path and content, no extension' => + [ 'foo', '', '', null, null ], + 'Non-empty path and content, with extension' => + [ 'foo.txt', 'foo', '/bogus/path', 'text/plain', null ], + 'Non-empty path and content, no extension' => + [ 'foo', 'foo', '/bogus/path', null, 'image/jpeg' ], + ]; + } +}