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
This commit is contained in:
parent
72886445ff
commit
3f1142045f
15 changed files with 285 additions and 153 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
|
|
|
|||
|
|
@ -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 `<body>`.
|
||||
* 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:
|
||||
|
|
|
|||
|
|
@ -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 ) ) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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] );
|
||||
|
|
|
|||
|
|
@ -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] );
|
||||
|
|
|
|||
|
|
@ -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__ ) {
|
||||
|
|
|
|||
|
|
@ -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( [] );
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue