From: Brad Jorsch Date: Tue, 28 Feb 2017 20:52:17 +0000 (-0500) Subject: Generalize ResourceLoader 'excludepage' functionality X-Git-Tag: 1.34.0-rc.0~5615 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/banques/Special:BookSources/9780123456?a=commitdiff_plain;h=3f1142045f51328197239aa62882881003bb9cdb;p=lhc%2Fweb%2Fwiklou.git Generalize ResourceLoader 'excludepage' functionality There has long been a hack for previewing edits to user JS/CSS, where OutputPage would pass an 'excludepage' parameter to ResourceLoaderUserModule to tell it not to load one particular page and would instead embed that page statically. That's nice, but there are other places where we could use the same thing. This patch generalizes it: * DerivativeResourceLoaderContext may now contain a callback for mapping titles to replacement Content objects. * ResourceLoaderWikiModule::getContent() uses the overrides, and requests embedding when they're used. All subclasses in Gerrit should pick it up automatically. * OutputPage gains methods for callers to add to the override mapping, which it passes on to RL. It loses a bunch of the special casing it had for the 'user' and 'user.styles' modules. * EditPage sets the overrides on OutputPage when doing the preview, as does ApiParse for prop=headhtml. TemplateSandbox does too in I83fa0856. * OutputPage::userCanPreview() gets less specific to editing user CSS and JS, since RL now handles the embedding based on the actual modules' dependencies and EditPage only requests it on preview. ApiParse also gets a new hook to support TemplateSandbox's API integration (used in I83fa0856). Bug: T112474 Change-Id: Ib9d2ce42931c1de8372e231314a1f672d7e2ac0e --- diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index 2eb3679ac7..366182b4dd 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 d932148e4d..b38bd666e4 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 a1d9ae82d5..fcf3d499a1 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 37527cf100..56df0f06eb 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 099d278f0c..05b4289b80 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 418d17f39a..b11bd6fd33 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 6c4a5d0664..bb8ab32998 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 c4e9884a1e..d41198ae55 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 8e213819f6..e747373e1a 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 8d8e008593..69e8a97a13 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 e87d28abc2..085244acf3 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 0a657d886f..a5a7364488 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 e4f58eb124..97ffd9413b 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 b226ee1caf..1b7e0fe401 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 db4494e08b..7a47a6360d 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(