Support redirects in JavaScriptContent

When a JavaScript page is moved, a "redirect" in the form of
mw.loader.load(...) will be left behind, so any other
JavaScript loading the page that way will still work, albeit
with an extra HTTP request.

This also implements Content::getRedirectTarget(), so redirects
are marked properly in the database, and users viewing them
are redirected properly. A magic "/* #REDIRECT */" comment
must be in front of the mw.loader.load call. This is done so
that pages which currently are just one mw.loader.load call
aren't turned into redirects.

Bug: 71200
Bug: 33973
Change-Id: I10fdff087a901da56fad64531f0e382f90ebcf37
This commit is contained in:
Kunal Mehta 2014-09-23 15:41:03 -07:00 committed by Krinkle
parent 06c7063501
commit ad9f14d662
7 changed files with 170 additions and 10 deletions

View file

@ -363,9 +363,8 @@ class MediaWiki {
$this->context->setWikiPage( $article->getPage() ); $this->context->setWikiPage( $article->getPage() );
} }
// NS_MEDIAWIKI has no redirects. // Skip some unnecessary code if the content model doesn't support redirects
// It is also used for CSS/JS, so performance matters here... if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) {
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
return $article; return $article;
} }

View file

@ -32,6 +32,11 @@
*/ */
class JavaScriptContent extends TextContent { class JavaScriptContent extends TextContent {
/**
* @var bool|Title|null
*/
private $redirectTarget = false;
/** /**
* @param string $text JavaScript code. * @param string $text JavaScript code.
* @param string $modelId the content model name * @param string $modelId the content model name
@ -73,4 +78,46 @@ class JavaScriptContent extends TextContent {
return $html; return $html;
} }
/**
* If this page is a redirect, return the content
* if it should redirect to $target instead
*
* @param Title $target
* @return JavaScriptContent
*/
public function updateRedirect( Title $target ) {
if ( !$this->isRedirect() ) {
return $this;
}
return $this->getContentHandler()->makeRedirectContent( $target );
}
/**
* @return Title|null
*/
public function getRedirectTarget() {
if ( $this->redirectTarget !== false ) {
return $this->redirectTarget;
}
$this->redirectTarget = null;
$text = $this->getNativeData();
if ( strpos( $text, '/* #REDIRECT */' ) === 0 ) {
// Extract the title from the url
preg_match( '/title=(.*?)\\\\u0026action=raw/', $text, $matches );
if ( isset( $matches[1] ) ) {
$title = Title::newFromText( $matches[1] );
if ( $title ) {
// Have a title, check that the current content equals what
// the redirect content should be
if ( $this->equals( $this->getContentHandler()->makeRedirectContent( $title ) ) ) {
$this->redirectTarget = $title;
}
}
}
}
return $this->redirectTarget;
}
} }

View file

@ -41,4 +41,22 @@ class JavaScriptContentHandler extends CodeContentHandler {
protected function getContentClass() { protected function getContentClass() {
return 'JavaScriptContent'; return 'JavaScriptContent';
} }
public function supportsRedirects() {
return true;
}
/**
* Create a redirect that is also valid JavaScript
*
* @param Title $destination
* @param string $text ignored
* @return JavaScriptContent
*/
public function makeRedirectContent( Title $destination, $text = '' ) {
// The parameters are passed as a string so the / is not url-encoded by wfArrayToCgi
$url = $destination->getFullURL( 'action=raw&ctype=text/javascript', false, PROTO_RELATIVE );
$class = $this->getContentClass();
return new $class( '/* #REDIRECT */' . Xml::encodeJsCall( 'mw.loader.load', array( $url ) ) );
}
} }

View file

@ -149,7 +149,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
*/ */
protected function getContent( $titleText ) { protected function getContent( $titleText ) {
$title = Title::newFromText( $titleText ); $title = Title::newFromText( $titleText );
if ( !$title || $title->isRedirect() ) { if ( !$title ) {
return null; return null;
} }

View file

@ -4,6 +4,8 @@
* @group ContentHandler * @group ContentHandler
* @group Database * @group Database
* ^--- needed, because we do need the database to test link updates * ^--- needed, because we do need the database to test link updates
*
* @FIXME this should not extend JavaScriptContentTest.
*/ */
class CssContentTest extends JavaScriptContentTest { class CssContentTest extends JavaScriptContentTest {
@ -68,6 +70,27 @@ class CssContentTest extends JavaScriptContentTest {
$this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() ); $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() );
} }
/**
* Redirects aren't supported
*/
public static function provideUpdateRedirect() {
return array(
array(
'#REDIRECT [[Someplace]]',
'#REDIRECT [[Someplace]]',
),
);
}
/**
* Override this since CssContent does not support redirects yet
*/
public static function provideGetRedirectTarget() {
return array(
array( null, '' ),
);
}
public static function dataEquals() { public static function dataEquals() {
return array( return array(
array( new CssContent( 'hallo' ), null, false ), array( new CssContent( 'hallo' ), null, false ),

View file

@ -0,0 +1,30 @@
<?php
class JavaScriptContentHandlerTest extends MediaWikiTestCase {
/**
* @dataProvider provideMakeRedirectContent
* @covers JavaScriptContentHandler::makeRedirectContent
*/
public function testMakeRedirectContent( $title, $expected ) {
$this->setMwGlobals( array(
'wgServer' => '//example.org',
'wgScript' => '/w/index.php',
) );
$ch = new JavaScriptContentHandler();
$content = $ch->makeRedirectContent( Title::newFromText( $title ) );
$this->assertInstanceOf( 'JavaScriptContent', $content );
$this->assertEquals( $expected, $content->serialize( CONTENT_FORMAT_JAVASCRIPT ) );
}
/**
* Keep this in sync with JavaScriptContentTest::provideGetRedirectTarget()
*/
public static function provideMakeRedirectContent() {
return array(
array( 'MediaWiki:MonoBook.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");' ),
array( 'User:FooBar/common.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=User:FooBar/common.js\u0026action=raw\u0026ctype=text/javascript");' ),
array( 'Gadget:FooBaz.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=Gadget:FooBaz.js\u0026action=raw\u0026ctype=text/javascript");' ),
);
}
}

View file

@ -251,16 +251,31 @@ class JavaScriptContentTest extends TextContentTest {
/** /**
* @covers JavaScriptContent::updateRedirect * @covers JavaScriptContent::updateRedirect
* @dataProvider provideUpdateRedirect
*/ */
public function testUpdateRedirect() { public function testUpdateRedirect( $oldText, $expectedText) {
$this->setMwGlobals( array(
'wgServer' => '//example.org',
'wgScriptPath' => '/w/index.php',
) );
$target = Title::newFromText( "testUpdateRedirect_target" ); $target = Title::newFromText( "testUpdateRedirect_target" );
$content = $this->newContent( "#REDIRECT [[Someplace]]" ); $content = new JavaScriptContent( $oldText );
$newContent = $content->updateRedirect( $target ); $newContent = $content->updateRedirect( $target );
$this->assertTrue( $this->assertEquals( $expectedText, $newContent->getNativeData() );
$content->equals( $newContent ), }
"content should be unchanged since it's not wikitext"
public static function provideUpdateRedirect() {
return array(
array(
'#REDIRECT [[Someplace]]',
'#REDIRECT [[Someplace]]',
),
array(
'/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");',
'/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=TestUpdateRedirect_target\u0026action=raw\u0026ctype=text/javascript");'
)
); );
} }
@ -290,4 +305,32 @@ class JavaScriptContentTest extends TextContentTest {
array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ), array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ),
); );
} }
/**
* @dataProvider provideGetRedirectTarget
*/
public function testGetRedirectTarget( $title, $text ) {
$this->setMwGlobals( array(
'wgServer' => '//example.org',
'wgScriptPath' => '/w/index.php',
) );
$content = new JavaScriptContent( $text );
$target = $content->getRedirectTarget();
$this->assertEquals( $title, $target ? $target->getPrefixedText() : null );
}
/**
* Keep this in sync with JavaScriptContentHandlerTest::provideMakeRedirectContent()
*/
public static function provideGetRedirectTarget() {
return array(
array( 'MediaWiki:MonoBook.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");' ),
array( 'User:FooBar/common.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=User:FooBar/common.js\u0026action=raw\u0026ctype=text/javascript");' ),
array( 'Gadget:FooBaz.js', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=Gadget:FooBaz.js\u0026action=raw\u0026ctype=text/javascript");' ),
// No #REDIRECT comment
array( null, 'mw.loader.load("//example.org/w/index.php?title=MediaWiki:NoRedirect.js\u0026action=raw\u0026ctype=text/javascript");' ),
// Different domain
array( null, '/* #REDIRECT */mw.loader.load("//example.com/w/index.php?title=MediaWiki:OtherWiki.js\u0026action=raw\u0026ctype=text/javascript");' ),
);
}
} }