diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index 2eb3679ac78..366182b4ddd 100644 --- a/RELEASE-NOTES-1.32 +++ b/RELEASE-NOTES-1.32 @@ -14,7 +14,9 @@ production. * … === New features in 1.32 === -* … +* (T112474) Generalized the ResourceLoader mechanism for overriding modules + using a particular page during edit previews. +* Added 'ApiParseMakeOutputPage' hook. === External library changes in 1.32 === * … @@ -35,7 +37,7 @@ production. * … === Action API internal changes in 1.32 === -* … +* Added 'ApiParseMakeOutputPage' hook. === Languages updated in 1.32 === MediaWiki supports over 350 languages. Many localisations are updated diff --git a/docs/hooks.txt b/docs/hooks.txt index d932148e4d0..b38bd666e4d 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -467,6 +467,12 @@ can alter or append to the array. (url), 'width', 'height', 'alt', 'align'. - url: Url for the given title. +'ApiParseMakeOutputPage': Called when preparing the OutputPage object for +ApiParse. This is mainly intended for calling OutputPage::addContentOverride() +or OutputPage::addContentOverrideCallback(). +$module: ApiBase (which is also a ContextSource) +$output: OutputPage + 'ApiQuery::moduleManager': Called when ApiQuery has finished initializing its module manager. Can be used to conditionally register API query modules. $moduleManager: ApiModuleManager Module manager instance diff --git a/includes/EditPage.php b/includes/EditPage.php index a1d9ae82d58..fcf3d499a1f 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -3893,6 +3893,9 @@ ERROR; $previewHTML = $parserResult['html']; $this->mParserOutput = $parserOutput; $out->addParserOutputMetadata( $parserOutput ); + if ( $out->userCanPreview() ) { + $out->addContentOverride( $this->getTitle(), $content ); + } if ( count( $parserOutput->getWarnings() ) ) { $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 37527cf100a..56df0f06ebc 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -20,6 +20,7 @@ * @file */ +use MediaWiki\Linker\LinkTarget; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Session\SessionManager; @@ -155,9 +156,6 @@ class OutputPage extends ContextSource { /** @var ResourceLoaderContext */ private $rlClientContext; - /** @var string */ - private $rlUserModuleState; - /** @var array */ private $rlExemptStyleModules; @@ -295,6 +293,12 @@ class OutputPage extends ContextSource { /** @var array Profiling data */ private $limitReportJSData = []; + /** @var array Map Title to Content */ + private $contentOverrides = []; + + /** @var callable[] */ + private $contentOverrideCallbacks = []; + /** * Link: header contents */ @@ -622,6 +626,39 @@ class OutputPage extends ContextSource { $this->mTarget = $target; } + /** + * Add a mapping from a LinkTarget to a Content, for things like page preview. + * @see self::addContentOverrideCallback() + * @since 1.32 + * @param LinkTarget $target + * @param Content $content + */ + public function addContentOverride( LinkTarget $target, Content $content ) { + if ( !$this->contentOverrides ) { + // Register a callback for $this->contentOverrides on the first call + $this->addContentOverrideCallback( function ( LinkTarget $target ) { + $key = $target->getNamespace() . ':' . $target->getDBkey(); + return isset( $this->contentOverrides[$key] ) + ? $this->contentOverrides[$key] + : null; + } ); + } + + $key = $target->getNamespace() . ':' . $target->getDBkey(); + $this->contentOverrides[$key] = $content; + } + + /** + * Add a callback for mapping from a Title to a Content object, for things + * like page preview. + * @see ResourceLoaderContext::getContentOverrideCallback() + * @since 1.32 + * @param callable $callback + */ + public function addContentOverrideCallback( callable $callback ) { + $this->contentOverrideCallbacks[] = $callback; + } + /** * Get an array of head items * @@ -2723,6 +2760,18 @@ class OutputPage extends ContextSource { $this->getResourceLoader(), new FauxRequest( $query ) ); + if ( $this->contentOverrideCallbacks ) { + $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext ); + $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) { + foreach ( $this->contentOverrideCallbacks as $callback ) { + $content = call_user_func( $callback, $title ); + if ( $content !== null ) { + return $content; + } + } + return null; + } ); + } } return $this->rlClientContext; } @@ -2743,6 +2792,7 @@ class OutputPage extends ContextSource { $context = $this->getRlClientContext(); $rl = $this->getResourceLoader(); $this->addModules( [ + 'user', 'user.options', 'user.tokens', ] ); @@ -2771,11 +2821,6 @@ class OutputPage extends ContextSource { function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) { $module = $rl->getModule( $name ); if ( $module ) { - if ( $name === 'user.styles' && $this->isUserCssPreview() ) { - $exemptStates[$name] = 'ready'; - // Special case in buildExemptModules() - return false; - } $group = $module->getGroup(); if ( isset( $exemptGroups[$group] ) ) { $exemptStates[$name] = 'ready'; @@ -2791,18 +2836,6 @@ class OutputPage extends ContextSource { ); $this->rlExemptStyleModules = $exemptGroups; - $isUserModuleFiltered = !$this->filterModules( [ 'user' ] ); - // If this page filters out 'user', makeResourceLoaderLink will drop it. - // Avoid indefinite "loading" state or untrue "ready" state (T145368). - if ( !$isUserModuleFiltered ) { - // Manually handled by getBottomScripts() - $userModule = $rl->getModule( 'user' ); - $userState = $userModule->isKnownEmpty( $context ) && !$this->isUserJsPreview() - ? 'ready' - : 'loading'; - $this->rlUserModuleState = $exemptStates['user'] = $userState; - } - $rlClient = new ResourceLoaderClientHtml( $context, [ 'target' => $this->getTarget(), ] ); @@ -2959,20 +2992,6 @@ class OutputPage extends ContextSource { return WrappedString::join( "\n", $chunks ); } - private function isUserJsPreview() { - return $this->getConfig()->get( 'AllowUserJs' ) - && $this->getTitle() - && $this->getTitle()->isUserJsConfigPage() - && $this->userCanPreview(); - } - - protected function isUserCssPreview() { - return $this->getConfig()->get( 'AllowUserCss' ) - && $this->getTitle() - && $this->getTitle()->isUserCssConfigPage() - && $this->userCanPreview(); - } - /** * JS stuff to put at the bottom of the ``. * These are legacy scripts ($this->mScripts), and user JS. @@ -2986,40 +3005,6 @@ class OutputPage extends ContextSource { // Legacy non-ResourceLoader scripts $chunks[] = $this->mScripts; - // Exempt 'user' module - // - May need excludepages for live preview. (T28283) - // - Must use TYPE_COMBINED so its response is handled by mw.loader.implement() which - // ensures execution is scheduled after the "site" module. - // - Don't load if module state is already resolved as "ready". - if ( $this->rlUserModuleState === 'loading' ) { - if ( $this->isUserJsPreview() ) { - $chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED, - [ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ] - ); - $chunks[] = ResourceLoader::makeInlineScript( - Xml::encodeJsCall( 'mw.loader.using', [ - [ 'user', 'site' ], - new XmlJsCode( - 'function () {' - . Xml::encodeJsCall( '$.globalEval', [ - $this->getRequest()->getText( 'wpTextbox1' ) - ] ) - . '}' - ) - ] ) - ); - // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded - // asynchronously and may arrive *after* the inline script here. So the previewed code - // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js. - // Similarly, when previewing ./common.js and the user module does arrive first, - // it will arrive without common.js and the inline script runs after. - // Thus running common after the excluded subpage. - } else { - // Load normally - $chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED ); - } - } - if ( $this->limitReportJSData ) { $chunks[] = ResourceLoader::makeInlineScript( ResourceLoader::makeConfigSetScript( @@ -3193,7 +3178,7 @@ class OutputPage extends ContextSource { /** * To make it harder for someone to slip a user a fake - * user-JavaScript or user-CSS preview, a random token + * JavaScript or CSS preview, a random token * is associated with the login session. If it's not * passed back with the preview request, we won't render * the code. @@ -3204,7 +3189,6 @@ class OutputPage extends ContextSource { $request = $this->getRequest(); if ( $request->getVal( 'action' ) !== 'submit' || - !$request->getCheck( 'wpPreview' ) || !$request->wasPosted() ) { return false; @@ -3221,17 +3205,6 @@ class OutputPage extends ContextSource { } $title = $this->getTitle(); - if ( - !$title->isUserJsConfigPage() - && !$title->isUserCssConfigPage() - ) { - return false; - } - if ( !$title->isSubpageOf( $user->getUserPage() ) ) { - // Don't execute another user's CSS or JS on preview (T85855) - return false; - } - $errors = $title->getUserPermissionsErrors( 'edit', $user ); if ( count( $errors ) !== 0 ) { return false; @@ -3570,29 +3543,10 @@ class OutputPage extends ContextSource { * @return string|WrappedStringList HTML */ protected function buildExemptModules() { - global $wgContLang; - $chunks = []; // Things that go after the ResourceLoaderDynamicStyles marker $append = []; - // Exempt 'user' styles module (may need 'excludepages' for live preview) - if ( $this->isUserCssPreview() ) { - $append[] = $this->makeResourceLoaderLink( - 'user.styles', - ResourceLoaderModule::TYPE_STYLES, - [ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ] - ); - - // Load the previewed CSS. Janus it if needed. - // User-supplied CSS is assumed to in the wiki's content language. - $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' ); - if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) { - $previewedCSS = CSSJanus::transform( $previewedCSS, true, false ); - } - $append[] = Html::inlineStyle( $previewedCSS ); - } - // We want site, private and user styles to override dynamically added styles from // general modules, but we want dynamically added styles to override statically added // style modules. So the order has to be: diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 099d278f0ca..05b4289b804 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -314,6 +314,9 @@ class ApiParse extends ApiBase { $outputPage = new OutputPage( $context ); $outputPage->addParserOutputMetadata( $p_result ); + if ( $this->content ) { + $outputPage->addContentOverride( $titleObj, $this->content ); + } $context->setOutput( $outputPage ); if ( $skin ) { @@ -324,6 +327,8 @@ class ApiParse extends ApiBase { $outputPage->addModules( $group ); } } + + Hooks::run( 'ApiParseMakeOutputPage', [ $this, $outputPage ] ); } if ( !is_null( $oldid ) ) { diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php b/includes/resourceloader/DerivativeResourceLoaderContext.php index 418d17f39a0..b11bd6fd338 100644 --- a/includes/resourceloader/DerivativeResourceLoaderContext.php +++ b/includes/resourceloader/DerivativeResourceLoaderContext.php @@ -44,6 +44,7 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext { protected $only = self::INHERIT_VALUE; protected $version = self::INHERIT_VALUE; protected $raw = self::INHERIT_VALUE; + protected $contentOverrideCallback = self::INHERIT_VALUE; public function __construct( ResourceLoaderContext $context ) { $this->context = $context; @@ -196,4 +197,21 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext { return $this->context->getResourceLoader(); } + public function getContentOverrideCallback() { + if ( $this->contentOverrideCallback === self::INHERIT_VALUE ) { + return $this->context->getContentOverrideCallback(); + } + return $this->contentOverrideCallback; + } + + /** + * @see self::getContentOverrideCallback + * @since 1.32 + * @param callable|null|int $callback As per self::getContentOverrideCallback, + * or self::INHERIT_VALUE + */ + public function setContentOverrideCallback( $callback ) { + $this->contentOverrideCallback = $callback; + } + } diff --git a/includes/resourceloader/ResourceLoaderClientHtml.php b/includes/resourceloader/ResourceLoaderClientHtml.php index 6c4a5d06640..bb8ab329989 100644 --- a/includes/resourceloader/ResourceLoaderClientHtml.php +++ b/includes/resourceloader/ResourceLoaderClientHtml.php @@ -358,7 +358,9 @@ class ResourceLoaderClientHtml { } $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req ); // Allow caller to setVersion() and setModules() - return new DerivativeResourceLoaderContext( $context ); + $ret = new DerivativeResourceLoaderContext( $context ); + $ret->setContentOverrideCallback( $mainContext->getContentOverrideCallback() ); + return $ret; } /** diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index c4e9884a1ee..d41198ae553 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -341,6 +341,22 @@ class ResourceLoaderContext implements MessageLocalizer { return $this->imageObj; } + /** + * Return the replaced-content mapping callback + * + * When editing a page that's used to generate the scripts or styles of a + * ResourceLoaderWikiModule, a preview should use the to-be-saved version of + * the page rather than the current version in the database. A context + * supporting such previews should return a callback to return these + * mappings here. + * + * @since 1.32 + * @return callable|null Signature is `Content|null func( Title $t )` + */ + public function getContentOverrideCallback() { + return null; + } + /** * @return bool */ diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php index 8e213819f60..e747373e1a7 100644 --- a/includes/resourceloader/ResourceLoaderUserModule.php +++ b/includes/resourceloader/ResourceLoaderUserModule.php @@ -58,8 +58,9 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { } } - // Hack for T28283: Allow excluding pages for preview on a CSS/JS page. - // The excludepage parameter is set by OutputPage. + // This is obsolete since 1.32 (T112474). It was formerly used by + // OutputPage to implement previewing of user CSS and JS. + // @todo: Remove it once we're sure nothing else is using the parameter $excludepage = $context->getRequest()->getVal( 'excludepage' ); if ( isset( $pages[$excludepage] ) ) { unset( $pages[$excludepage] ); diff --git a/includes/resourceloader/ResourceLoaderUserStylesModule.php b/includes/resourceloader/ResourceLoaderUserStylesModule.php index 8d8e0085935..69e8a97a136 100644 --- a/includes/resourceloader/ResourceLoaderUserStylesModule.php +++ b/includes/resourceloader/ResourceLoaderUserStylesModule.php @@ -58,8 +58,9 @@ class ResourceLoaderUserStylesModule extends ResourceLoaderWikiModule { } } - // Hack for T28283: Allow excluding pages for preview on a CSS/JS page. - // The excludepage parameter is set by OutputPage. + // This is obsolete since 1.32 (T112474). It was formerly used by + // OutputPage to implement previewing of user CSS and JS. + // @todo: Remove it once we're sure nothing else is using the parameter $excludepage = $context->getRequest()->getVal( 'excludepage' ); if ( isset( $pages[$excludepage] ) ) { unset( $pages[$excludepage] ); diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index e87d28abc24..085244acf3b 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -157,24 +157,22 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { /** * @param string $titleText + * @param ResourceLoaderContext|null $context (but passing null is deprecated) * @return null|string + * @since 1.32 added the $context parameter */ - protected function getContent( $titleText ) { + protected function getContent( $titleText, ResourceLoaderContext $context = null ) { $title = Title::newFromText( $titleText ); if ( !$title ) { return null; // Bad title } - // If the page is a redirect, follow the redirect. - if ( $title->isRedirect() ) { - $content = $this->getContentObj( $title ); - $title = $content ? $content->getUltimateRedirectTarget() : null; - if ( !$title ) { - return null; // Dead redirect - } + $content = $this->getContentObj( $title, $context ); + if ( !$content ) { + return null; // No content found } - $handler = ContentHandler::getForTitle( $title ); + $handler = $content->getContentHandler(); if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) { $format = CONTENT_FORMAT_CSS; } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) { @@ -183,31 +181,81 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { return null; // Bad content model } - $content = $this->getContentObj( $title ); - if ( !$content ) { - return null; // No content found - } - return $content->serialize( $format ); } /** * @param Title $title + * @param ResourceLoaderContext|null $context (but passing null is deprecated) + * @param int|null $maxRedirects Maximum number of redirects to follow. If + * null, uses $wgMaxRedirects * @return Content|null + * @since 1.32 added the $context and $maxRedirects parameters */ - protected function getContentObj( Title $title ) { - $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title ); - if ( !$revision ) { - return null; + protected function getContentObj( + Title $title, ResourceLoaderContext $context = null, $maxRedirects = null + ) { + if ( $context === null ) { + wfDeprecated( __METHOD__ . ' without a ResourceLoader context', '1.32' ); } - $content = $revision->getContent( Revision::RAW ); - if ( !$content ) { - wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' ); - return null; + + $overrideCallback = $context ? $context->getContentOverrideCallback() : null; + $content = $overrideCallback ? call_user_func( $overrideCallback, $title ) : null; + if ( $content ) { + if ( !$content instanceof Content ) { + $this->getLogger()->error( + 'Bad content override for "{title}" in ' . __METHOD__, + [ 'title' => $title->getPrefixedText() ] + ); + return null; + } + } else { + $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title ); + if ( !$revision ) { + return null; + } + $content = $revision->getContent( Revision::RAW ); + + if ( !$content ) { + $this->getLogger()->error( + 'Failed to load content of JS/CSS page "{title}" in ' . __METHOD__, + [ 'title' => $title->getPrefixedText() ] + ); + return null; + } } + + if ( $content && $content->isRedirect() ) { + if ( $maxRedirects === null ) { + $maxRedirects = $this->getConfig()->get( 'MaxRedirects' ) ?: 0; + } + if ( $maxRedirects > 0 ) { + $newTitle = $content->getRedirectTarget(); + return $newTitle ? $this->getContentObj( $newTitle, $context, $maxRedirects - 1 ) : null; + } + } + return $content; } + /** + * @param ResourceLoaderContext $context + * @return bool + */ + public function shouldEmbedModule( ResourceLoaderContext $context ) { + $overrideCallback = $context->getContentOverrideCallback(); + if ( $overrideCallback && $this->getSource() === 'local' ) { + foreach ( $this->getPages( $context ) as $page => $info ) { + $title = Title::newFromText( $page ); + if ( $title && call_user_func( $overrideCallback, $title ) !== null ) { + return true; + } + } + } + + return parent::shouldEmbedModule( $context ); + } + /** * @param ResourceLoaderContext $context * @return string JavaScript code @@ -218,7 +266,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( $options['type'] !== 'script' ) { continue; } - $script = $this->getContent( $titleText ); + $script = $this->getContent( $titleText, $context ); if ( strval( $script ) !== '' ) { $script = $this->validateScriptFile( $titleText, $script ); $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n"; @@ -238,7 +286,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { continue; } $media = isset( $options['media'] ) ? $options['media'] : 'all'; - $style = $this->getContent( $titleText ); + $style = $this->getContent( $titleText, $context ); if ( strval( $style ) === '' ) { continue; } @@ -339,7 +387,26 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( !isset( $this->titleInfo[$batchKey] ) ) { $this->titleInfo[$batchKey] = static::fetchTitleInfo( $dbr, $pageNames, __METHOD__ ); } - return $this->titleInfo[$batchKey]; + + $titleInfo = $this->titleInfo[$batchKey]; + + // Override the title info from the overrides, if any + $overrideCallback = $context->getContentOverrideCallback(); + if ( $overrideCallback ) { + foreach ( $pageNames as $page ) { + $title = Title::newFromText( $page ); + $content = $title ? call_user_func( $overrideCallback, $title ) : null; + if ( $content !== null ) { + $titleInfo[$title->getPrefixedText()] = [ + 'page_len' => $content->getSize(), + 'page_latest' => 'TBD', // None available + 'page_touched' => wfTimestamp( TS_MW ), + ]; + } + } + } + + return $titleInfo; } protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) { diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php index 0a657d886f5..a5a7364488c 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -441,11 +441,8 @@ class OutputPageTest extends MediaWikiTestCase { $ctx->setLanguage( 'en' ); $outputPage = $this->getMockBuilder( OutputPage::class ) ->setConstructorArgs( [ $ctx ] ) - ->setMethods( [ 'isUserCssPreview', 'buildCssLinksArray' ] ) + ->setMethods( [ 'buildCssLinksArray' ] ) ->getMock(); - $outputPage->expects( $this->any() ) - ->method( 'isUserCssPreview' ) - ->willReturn( false ); $outputPage->expects( $this->any() ) ->method( 'buildCssLinksArray' ) ->willReturn( [] ); diff --git a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php index e4f58eb1240..97ffd9413b7 100644 --- a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php +++ b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php @@ -119,6 +119,21 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { $this->assertEquals( $derived->getHash(), 'nl|fallback||Example|scripts|||||' ); } + public function testContentOverrides() { + $derived = new DerivativeResourceLoaderContext( self::getContext() ); + + $this->assertNull( $derived->getContentOverrideCallback() ); + + $override = function ( Title $t ) { + return null; + }; + $derived->setContentOverrideCallback( $override ); + $this->assertSame( $override, $derived->getContentOverrideCallback() ); + + $derived2 = new DerivativeResourceLoaderContext( $derived ); + $this->assertSame( $override, $derived2->getContentOverrideCallback() ); + } + public function testAccessors() { $context = self::getContext(); $derived = new DerivativeResourceLoaderContext( $context ); diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php index b226ee1caf1..1b7e0fe4018 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php @@ -31,6 +31,7 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase { $this->assertEquals( null, $ctx->getOnly() ); $this->assertEquals( 'fallback', $ctx->getSkin() ); $this->assertEquals( null, $ctx->getUser() ); + $this->assertNull( $ctx->getContentOverrideCallback() ); // Misc $this->assertEquals( 'ltr', $ctx->getDirection() ); diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php index db4494e08b0..7a47a6360dd 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php @@ -351,38 +351,82 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { $module = TestingAccessWrapper::newFromObject( $module ); $this->assertEquals( $expected, - $module->getContent( $titleText ) + $module->getContent( $titleText, $context ) ); } /** * @covers ResourceLoaderWikiModule::getContent + * @covers ResourceLoaderWikiModule::getContentObj + * @covers ResourceLoaderWikiModule::shouldEmbedModule + */ + public function testContentOverrides() { + $pages = [ + 'MediaWiki:Common.css' => [ 'type' => 'style' ], + ]; + + $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class ) + ->setMethods( [ 'getPages' ] ) + ->getMock(); + $module->method( 'getPages' )->willReturn( $pages ); + + $rl = new EmptyResourceLoader(); + $rl->register( 'testmodule', $module ); + $context = new DerivativeResourceLoaderContext( + new ResourceLoaderContext( $rl, new FauxRequest() ) + ); + $context->setContentOverrideCallback( function ( Title $t ) { + if ( $t->getPrefixedText() === 'MediaWiki:Common.css' ) { + return new CssContent( '.override{}' ); + } + return null; + } ); + + $this->assertTrue( $module->shouldEmbedModule( $context ) ); + $this->assertEquals( [ + 'all' => [ + "/*\nMediaWiki:Common.css\n*/\n.override{}" + ] + ], $module->getStyles( $context ) ); + + $context->setContentOverrideCallback( function ( Title $t ) { + if ( $t->getPrefixedText() === 'MediaWiki:Skin.css' ) { + return new CssContent( '.override{}' ); + } + return null; + } ); + $this->assertFalse( $module->shouldEmbedModule( $context ) ); + } + + /** + * @covers ResourceLoaderWikiModule::getContent + * @covers ResourceLoaderWikiModule::getContentObj */ public function testGetContentForRedirects() { // Set up context and module object - $context = $this->getResourceLoaderContext( [], new EmptyResourceLoader ); + $context = new DerivativeResourceLoaderContext( + $this->getResourceLoaderContext( [], new EmptyResourceLoader ) + ); $module = $this->getMockBuilder( ResourceLoaderWikiModule::class ) - ->setMethods( [ 'getPages', 'getContentObj' ] ) + ->setMethods( [ 'getPages' ] ) ->getMock(); $module->expects( $this->any() ) ->method( 'getPages' ) ->will( $this->returnValue( [ 'MediaWiki:Redirect.js' => [ 'type' => 'script' ] ] ) ); - $module->expects( $this->any() ) - ->method( 'getContentObj' ) - ->will( $this->returnCallback( function ( Title $title ) { - if ( $title->getPrefixedText() === 'MediaWiki:Redirect.js' ) { - $handler = new JavaScriptContentHandler(); - return $handler->makeRedirectContent( - Title::makeTitle( NS_MEDIAWIKI, 'Target.js' ) - ); - } elseif ( $title->getPrefixedText() === 'MediaWiki:Target.js' ) { - return new JavaScriptContent( 'target;' ); - } else { - return null; - } - } ) ); + $context->setContentOverrideCallback( function ( Title $title ) { + if ( $title->getPrefixedText() === 'MediaWiki:Redirect.js' ) { + $handler = new JavaScriptContentHandler(); + return $handler->makeRedirectContent( + Title::makeTitle( NS_MEDIAWIKI, 'Target.js' ) + ); + } elseif ( $title->getPrefixedText() === 'MediaWiki:Target.js' ) { + return new JavaScriptContent( 'target;' ); + } else { + return null; + } + } ); // Mock away Title's db queries with LinkCache MediaWikiServices::getInstance()->getLinkCache()->addGoodLinkObj(