Anomie has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/340768 )

Change subject: Generalize ResourceLoader 'excludepage' functionality
......................................................................

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.
* ResourceLoaderModule has a method to indicate whether the module
  should be embedded.
* ResourceLoaderClientHtml embeds any module that returns true from this
  new method (like how it already embeds 'private'-group modules), so
  OutputPage doesn't have to worry about it.
* 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.

Bug: T112474
Change-Id: Ib9d2ce42931c1de8372e231314a1f672d7e2ac0e
---
M RELEASE-NOTES-1.29
M docs/hooks.txt
M includes/EditPage.php
M includes/OutputPage.php
M includes/api/ApiParse.php
M includes/resourceloader/DerivativeResourceLoaderContext.php
M includes/resourceloader/ResourceLoaderClientHtml.php
M includes/resourceloader/ResourceLoaderContext.php
M includes/resourceloader/ResourceLoaderModule.php
M includes/resourceloader/ResourceLoaderUserModule.php
M includes/resourceloader/ResourceLoaderWikiModule.php
M tests/phpunit/ResourceLoaderTestCase.php
M tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
M tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
M tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
M tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
16 files changed, 315 insertions(+), 123 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/68/340768/1

diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29
index 9f2c850..8682bda 100644
--- a/RELEASE-NOTES-1.29
+++ b/RELEASE-NOTES-1.29
@@ -54,6 +54,9 @@
   This might affect some forms that used them and only worked because the
   attributes were not actually being set.
 * Expiry times can now be specified when users are added to user groups.
+* (T112474) Generalized the ResourceLoader mechanism for overriding modules
+  using a particular page during edit previews.
+* Added 'ApiParseOutputPageForHeadHtml' hook.
 
 === External library changes in 1.29 ===
 
@@ -129,6 +132,7 @@
   various methods now take a module path rather than a module name.
 * ApiMessageTrait::getApiCode() now strips 'apierror-' and 'apiwarn-' prefixes
   from the message key, and maps some message keys for backwards compatibility.
+* Added 'ApiParseOutputPageForHeadHtml' hook.
 
 === Languages updated in 1.29 ===
 
diff --git a/docs/hooks.txt b/docs/hooks.txt
index 846a073..4f4d771 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -469,6 +469,11 @@
       (url), 'width', 'height', 'alt', 'align'.
     - url: Url for the given title.
 
+'ApiParseOutputPageForHeadHtml': Called when preparing the OutputPage object
+for ApiParse's headhtml output property.
+$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 a02392e..3f972c1 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -3786,6 +3786,9 @@
                        $previewHTML = $parserResult['html'];
                        $this->mParserOutput = $parserOutput;
                        $wgOut->addParserOutputMetadata( $parserOutput );
+                       if ( $wgOut->userCanPreview() ) {
+                               $wgOut->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 9ecfa23..55bb365 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -157,9 +157,6 @@
        /** @var ResourceLoaderContext */
        private $rlClientContext;
 
-       /** @var string */
-       private $rlUserModuleState;
-
        /** @var array */
        private $rlExemptStyleModules;
 
@@ -301,6 +298,12 @@
 
        /** @var array Profiling data */
        private $limitReportJSData = [];
+
+       /** @var array Map Title to Content */
+       private $contentOverrides = [];
+
+       /** @var callable[] */
+       private $contentOverrideCallbacks = [];
 
        /**
         * Constructor for OutputPage. This should not be called directly.
@@ -652,6 +655,36 @@
         */
        public function setTarget( $target ) {
                $this->mTarget = $target;
+       }
+
+       /**
+        * Add a mapping from a Title to a Content, for things like page 
preview.
+        * @see self::addContentOverrideCallback()
+        * @since 1.29
+        * @param Title $title
+        * @param Content $content
+        */
+       public function addContentOverride( Title $title, Content $content ) {
+               if ( !$this->contentOverrides ) {
+                       // Register a callback for $this->contentOverrides on 
the first call
+                       $this->addContentOverrideCallback( function ( Title 
$title ) {
+                               return isset( 
$this->contentOverrides[$title->getPrefixedText()] )
+                                       ? 
$this->contentOverrides[$title->getPrefixedText()]
+                                       : null;
+                       } );
+               }
+               $this->contentOverrides[$title->getPrefixedText()] = $content;
+       }
+
+       /**
+        * Add a callback for mapping from a Title to a Content object, for 
things
+        * like page preview.
+        * @see ResourceLoaderContext::getContentOverrideCallback()
+        * @since 1.29
+        * @param callable $callback
+        */
+       public function addContentOverrideCallback( callable $callback ) {
+               $this->contentOverrideCallbacks[] = $callback;
        }
 
        /**
@@ -2748,6 +2781,18 @@
                                $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;
        }
@@ -2768,6 +2813,7 @@
                        $context = $this->getRlClientContext();
                        $rl = $this->getResourceLoader();
                        $this->addModules( [
+                               'user',
                                'user.options',
                                'user.tokens',
                        ] );
@@ -2796,11 +2842,6 @@
                                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';
@@ -2815,18 +2856,6 @@
                                }
                        );
                        $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, 
$this->getTarget() );
                        $rlClient->setConfig( $this->getJSVars() );
@@ -2971,20 +3000,6 @@
                return WrappedString::join( "\n", $chunks );
        }
 
-       private function isUserJsPreview() {
-               return $this->getConfig()->get( 'AllowUserJs' )
-                       && $this->getTitle()
-                       && $this->getTitle()->isJsSubpage()
-                       && $this->userCanPreview();
-       }
-
-       private function isUserCssPreview() {
-               return $this->getConfig()->get( 'AllowUserCss' )
-                       && $this->getTitle()
-                       && $this->getTitle()->isCssSubpage()
-                       && $this->userCanPreview();
-       }
-
        /**
         * JS stuff to put at the bottom of the `<body>`. These are modules 
with position 'bottom',
         * legacy scripts ($this->mScripts), and user JS.
@@ -2997,40 +3012,6 @@
 
                // 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(
@@ -3199,7 +3180,7 @@
 
        /**
         * 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.
@@ -3210,7 +3191,6 @@
                $request = $this->getRequest();
                if (
                        $request->getVal( 'action' ) !== 'submit' ||
-                       !$request->getCheck( 'wpPreview' ) ||
                        !$request->wasPosted()
                ) {
                        return false;
@@ -3227,14 +3207,6 @@
                }
 
                $title = $this->getTitle();
-               if ( !$title->isJsSubpage() && !$title->isCssSubpage() ) {
-                       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;
@@ -3581,28 +3553,9 @@
         * @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
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 287ffb7..a644308 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -360,6 +360,8 @@
                        // RequestContext at the root of the stack.
                        $output = new OutputPage( $context );
                        $output->addParserOutputMetadata( $p_result );
+                       $output->addContentOverride( $titleObj, $this->content 
);
+                       Hooks::run( 'ApiParseOutputPageForHeadHtml', [ $this, 
$output ] );
 
                        $result_array['headhtml'] = $output->headElement( 
$context->getSkin() );
                        $result_array[ApiResult::META_BC_SUBELEMENTS][] = 
'headhtml';
diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php 
b/includes/resourceloader/DerivativeResourceLoaderContext.php
index 418d17f..d216f82 100644
--- a/includes/resourceloader/DerivativeResourceLoaderContext.php
+++ b/includes/resourceloader/DerivativeResourceLoaderContext.php
@@ -44,6 +44,7 @@
        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,20 @@
                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.29
+        * @param callable|null|int 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 8c792ad..20763ed 100644
--- a/includes/resourceloader/ResourceLoaderClientHtml.php
+++ b/includes/resourceloader/ResourceLoaderClientHtml.php
@@ -149,7 +149,7 @@
                                continue;
                        }
 
-                       $group = $module->getGroup();
+                       $group = $module->shouldEmbedModule( $this->context ) ? 
'private' : $module->getGroup();
 
                        if ( $group === 'private' ) {
                                // Embed via mw.loader.implement per T36907.
@@ -179,7 +179,7 @@
                                $data['states'][$name] = 'ready';
                        }
 
-                       $group = $module->getGroup();
+                       $group = $module->shouldEmbedModule( $this->context ) ? 
'private' : $module->getGroup();
                        $context = $this->getContext( $group, 
ResourceLoaderModule::TYPE_STYLES );
                        if ( $module->isKnownEmpty( $context ) ) {
                                // Avoid needless request for empty module
@@ -203,7 +203,7 @@
                                continue;
                        }
 
-                       $group = $module->getGroup();
+                       $group = $module->shouldEmbedModule( $this->context ) ? 
'private' : $module->getGroup();
                        $context = $this->getContext( $group, 
ResourceLoaderModule::TYPE_SCRIPTS );
                        if ( $module->isKnownEmpty( $context ) ) {
                                // Avoid needless request for empty module
@@ -347,7 +347,9 @@
                }
                $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;
        }
 
        /**
@@ -392,25 +394,44 @@
                foreach ( $sortedModules as $source => $groups ) {
                        foreach ( $groups as $group => $grpModules ) {
                                $context = self::makeContext( $mainContext, 
$group, $only, $extraQuery );
-                               $context->setModules( array_keys( $grpModules ) 
);
 
+                               // Separate linked and embedded modules
+                               $embedModules = [];
+                               $linkModules = [];
                                if ( $group === 'private' ) {
+                                       $embedModules = $grpModules;
+                               } else {
+                                       foreach ( $grpModules as $name => 
$module ) {
+                                               if ( 
$module->shouldEmbedModule( $context ) ) {
+                                                       $embedModules[$name] = 
$module;
+                                               } else {
+                                                       $linkModules[$name] = 
$module;
+                                               }
+                                       }
+                               }
+
+                               if ( $embedModules ) {
+                                       $context->setModules( array_keys( 
$embedModules ) );
                                        // Decide whether to use style or 
script element
                                        if ( $only == 
ResourceLoaderModule::TYPE_STYLES ) {
                                                $chunks[] = Html::inlineStyle(
-                                                       
$rl->makeModuleResponse( $context, $grpModules )
+                                                       
$rl->makeModuleResponse( $context, $embedModules )
                                                );
                                        } else {
                                                $chunks[] = 
ResourceLoader::makeInlineScript(
-                                                       
$rl->makeModuleResponse( $context, $grpModules )
+                                                       
$rl->makeModuleResponse( $context, $embedModules )
                                                );
                                        }
+                               }
+
+                               if ( !$linkModules ) {
                                        continue;
                                }
+                               $context->setModules( array_keys( $linkModules 
) );
 
                                // See if we have one or more raw modules
                                $isRaw = false;
-                               foreach ( $grpModules as $key => $module ) {
+                               foreach ( $linkModules as $key => $module ) {
                                        $isRaw |= $module->isRaw();
                                }
 
diff --git a/includes/resourceloader/ResourceLoaderContext.php 
b/includes/resourceloader/ResourceLoaderContext.php
index 8955b8c..ea0cedb 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -342,6 +342,22 @@
        }
 
        /**
+        * Return the replaced-content mapping callback
+        *
+        * When editing a page that's used to generate the scripts or styles of 
a
+        * ResourceLoaderModule, 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.29
+        * @return callable|null Signature is `Content|null func( Title $t )`
+        */
+       public function getContentOverrideCallback() {
+               return null;
+       }
+
+       /**
         * @return bool
         */
        public function shouldIncludeScripts() {
diff --git a/includes/resourceloader/ResourceLoaderModule.php 
b/includes/resourceloader/ResourceLoaderModule.php
index d4dabe7..c2d6395 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -918,6 +918,20 @@
                return false;
        }
 
+       /**
+        * Check whether this module should be embeded rather than linked
+        *
+        * Modules returning true here will be embedded rather than loaded by
+        * ResourceLoaderClientHtml.
+        *
+        * @since 1.29
+        * @param ResourceLoaderContext $context
+        * @return bool
+        */
+       public function shouldEmbedModule( ResourceLoaderContext $context ) {
+               return false;
+       }
+
        /** @var JSParser Lazy-initialized; use self::javaScriptParser() */
        private static $jsParser;
        private static $parseCacheVersion = 1;
diff --git a/includes/resourceloader/ResourceLoaderUserModule.php 
b/includes/resourceloader/ResourceLoaderUserModule.php
index 8f58040..b4dcbe2 100644
--- a/includes/resourceloader/ResourceLoaderUserModule.php
+++ b/includes/resourceloader/ResourceLoaderUserModule.php
@@ -58,8 +58,9 @@
                        }
                }
 
-               // 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.29 (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 b0d060b..1d6f245 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -141,15 +141,32 @@
 
        /**
         * @param string $titleText
+        * @param ResourceLoaderContext|null $context (but passing null is 
deprecated)
         * @return null|string
+        * @since 1.29 added the $context parameter
         */
-       protected function getContent( $titleText ) {
+       protected function getContent( $titleText, ResourceLoaderContext 
$context = null ) {
+               if ( $context === null ) {
+                       wfDeprecated( __METHOD__ . ' without a ResourceLoader 
context', '1.29' );
+               }
+
                $title = Title::newFromText( $titleText );
                if ( !$title ) {
                        return null;
                }
 
-               $handler = ContentHandler::getForTitle( $title );
+               $overrideCallback = $context ? 
$context->getContentOverrideCallback() : null;
+               $content = $overrideCallback ? call_user_func( 
$overrideCallback, $title ) : null;
+               if ( $content ) {
+                       if ( !$content instanceof Content ) {
+                               wfDebugLog( 'resourceloader', __METHOD__ . ": 
bad content override for $titleText!" );
+                               return null;
+                       }
+                       $handler = $content->getContentHandler();
+               } else {
+                       $handler = ContentHandler::getForTitle( $title );
+               }
+
                if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
                        $format = CONTENT_FORMAT_CSS;
                } elseif ( $handler->isSupportedFormat( 
CONTENT_FORMAT_JAVASCRIPT ) ) {
@@ -158,20 +175,40 @@
                        return null;
                }
 
-               $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), 
$title->getArticleID(),
-                       $title->getLatestRevID() );
-               if ( !$revision ) {
-                       return null;
-               }
-               $revision->setTitle( $title );
-               $content = $revision->getContent( Revision::RAW );
-
                if ( !$content ) {
-                       wfDebugLog( 'resourceloader', __METHOD__ . ': failed to 
load content of JS/CSS page!' );
-                       return null;
+                       $revision = Revision::newKnownCurrent( wfGetDB( 
DB_REPLICA ), $title->getArticleID(),
+                               $title->getLatestRevID() );
+                       if ( !$revision ) {
+                               return null;
+                       }
+                       $revision->setTitle( $title );
+                       $content = $revision->getContent( Revision::RAW );
+
+                       if ( !$content ) {
+                               wfDebugLog( 'resourceloader', __METHOD__ . ': 
failed to load content of JS/CSS page!' );
+                               return null;
+                       }
                }
 
                return $content->serialize( $format );
+       }
+
+       /**
+        * @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 );
        }
 
        /**
@@ -184,7 +221,7 @@
                        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";
@@ -204,7 +241,7 @@
                                continue;
                        }
                        $media = isset( $options['media'] ) ? $options['media'] 
: 'all';
-                       $style = $this->getContent( $titleText );
+                       $style = $this->getContent( $titleText, $context );
                        if ( strval( $style ) === '' ) {
                                continue;
                        }
@@ -294,7 +331,25 @@
                sort( $pageNames );
                $key = implode( '|', $pageNames );
                if ( !isset( $this->titleInfo[$key] ) ) {
-                       $this->titleInfo[$key] = static::fetchTitleInfo( $dbr, 
$pageNames, __METHOD__ );
+                       $titleInfo = static::fetchTitleInfo( $dbr, $pageNames, 
__METHOD__ );
+
+                       // Override the title info from the overrides, if any
+                       $overrideCallback = 
$context->getContentOverrideCallback();
+                       if ( $overrideCallback ) {
+                               foreach ( $pageNames as $page => $info ) {
+                                       $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',
+                                                       'page_touched' => 
wfTimestamp( TS_MW ),
+                                               ];
+                                       }
+                               }
+                       }
+
+                       $this->titleInfo[$key] = $titleInfo;
                }
                return $this->titleInfo[$key];
        }
diff --git a/tests/phpunit/ResourceLoaderTestCase.php 
b/tests/phpunit/ResourceLoaderTestCase.php
index 68b91bf..4cf20c4 100644
--- a/tests/phpunit/ResourceLoaderTestCase.php
+++ b/tests/phpunit/ResourceLoaderTestCase.php
@@ -93,6 +93,7 @@
        protected $isKnownEmpty = false;
        protected $type = ResourceLoaderModule::LOAD_GENERAL;
        protected $targets = [ 'phpunit' ];
+       protected $shouldEmbed = false;
 
        public function __construct( $options = [] ) {
                foreach ( $options as $key => $value ) {
@@ -142,6 +143,10 @@
                return $this->isKnownEmpty;
        }
 
+       public function shouldEmbedModule( ResourceLoaderContext $context ) {
+               return $this->shouldEmbed;
+       }
+
        public function enableModuleContentVersion() {
                return true;
        }
diff --git 
a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php 
b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
index 0be04ef..eb7d2ce 100644
--- 
a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
+++ 
b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
@@ -117,6 +117,21 @@
                $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/ResourceLoaderClientHtmlTest.php 
b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
index 2db3c16..e38b6b4 100644
--- a/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
@@ -41,6 +41,7 @@
                        'test.top' => [ 'position' => 'top' ],
                        'test.private.top' => [ 'group' => 'private', 
'position' => 'top' ],
                        'test.private.bottom' => [ 'group' => 'private', 
'position' => 'bottom' ],
+                       'test.shouldembed' => [ 'shouldEmbed' => true ],
 
                        'test.styles.pure' => [ 'type' => 
ResourceLoaderModule::LOAD_STYLES ],
                        'test.styles.mixed' => [],
@@ -48,12 +49,14 @@
                        'test.styles.mixed.user' => [ 'group' => 'user' ],
                        'test.styles.mixed.user.empty' => [ 'group' => 'user', 
'isKnownEmpty' => true ],
                        'test.styles.private' => [ 'group' => 'private', 
'styles' => '.private{}' ],
+                       'test.styles.shouldembed' => [ 'shouldEmbed' => true, 
'styles' => '.shouldembed{}' ],
 
                        'test.scripts' => [],
                        'test.scripts.top' => [ 'position' => 'top' ],
                        'test.scripts.mixed.user' => [ 'group' => 'user' ],
                        'test.scripts.mixed.user.empty' => [ 'group' => 'user', 
'isKnownEmpty' => true ],
                        'test.scripts.raw' => [ 'isRaw' => true ],
+                       'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
                ];
                return array_map( function ( $options ) {
                        return self::makeModule( $options );
@@ -86,6 +89,7 @@
                        'test.private.bottom',
                        'test.private.top',
                        'test.top',
+                       'test.shouldembed',
                        'test.unregistered',
                ] );
                $client->setModuleStyles( [
@@ -93,12 +97,14 @@
                        'test.styles.mixed.user.empty',
                        'test.styles.private',
                        'test.styles.pure',
+                       'test.styles.shouldembed',
                        'test.unregistered.styles',
                ] );
                $client->setModuleScripts( [
                        'test.scripts',
                        'test.scripts.mixed.user.empty',
                        'test.scripts.top',
+                       'test.scripts.shouldembed',
                        'test.unregistered.scripts',
                ] );
 
@@ -106,12 +112,15 @@
                        'states' => [
                                'test.private.top' => 'loading',
                                'test.private.bottom' => 'loading',
+                               'test.shouldembed' => 'loading',
                                'test.styles.pure' => 'ready',
                                'test.styles.mixed.user.empty' => 'ready',
                                'test.styles.private' => 'ready',
+                               'test.styles.shouldembed' => 'ready',
                                'test.scripts' => 'loading',
                                'test.scripts.top' => 'loading',
                                'test.scripts.mixed.user.empty' => 'ready',
+                               'test.scripts.shouldembed' => 'loading',
                        ],
                        'general' => [
                                'test',
@@ -124,12 +133,14 @@
                        'scripts' => [
                                'test.scripts',
                                'test.scripts.top',
+                               'test.scripts.shouldembed',
                        ],
                        'embed' => [
-                               'styles' => [ 'test.styles.private' ],
+                               'styles' => [ 'test.styles.private', 
'test.styles.shouldembed' ],
                                'general' => [
                                        'test.private.bottom',
                                        'test.private.top',
+                                       'test.shouldembed',
                                ],
                        ],
                ];
@@ -261,6 +272,37 @@
                                'only' => ResourceLoaderModule::TYPE_STYLES,
                                'output' => '<noscript><link rel="stylesheet" 
href="/w/load.php?debug=false&amp;lang=nl&amp;modules=test.styles.noscript&amp;only=styles&amp;skin=fallback"/></noscript>',
                        ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'output' => 
'<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",function($,jQuery,require,module){},{"css":[]});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'output' => '<style>.shouldembed{}</style>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.scripts.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
+                               'output' => 
'<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test', 'test.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'output' => 
'<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",function($,jQuery,require,module){},{"css":[]});mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test\u0026skin=fallback");});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.pure', 
'test.styles.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'output' => '<style>.shouldembed{}</style>' . 
"\n"
+                                       . '<link rel="stylesheet" 
href="/w/load.php?debug=false&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles&amp;skin=fallback"/>',
+                       ],
                        // @codingStandardsIgnoreEnd
                ];
        }
diff --git 
a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php 
b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
index b658efb..33dd097 100644
--- a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
@@ -28,6 +28,7 @@
                $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 a332528..a0baa30 100644
--- a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
@@ -214,6 +214,44 @@
                $module = TestingAccessWrapper::newFromObject( $module );
                $this->assertEquals( $expected, $module->getTitleInfo( $context 
), 'Title info' );
        }
+
+       public function testContentOverrides() {
+               $pages = [
+                       'MediaWiki:Common.css' => [ 'type' => 'style' ],
+               ];
+
+               $module = $this->getMockBuilder( 'TestResourceLoaderWikiModule' 
)
+                       ->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 ) );
+       }
 }
 
 class TestResourceLoaderWikiModule extends ResourceLoaderWikiModule {

-- 
To view, visit https://gerrit.wikimedia.org/r/340768
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib9d2ce42931c1de8372e231314a1f672d7e2ac0e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Anomie <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to